I have seen this question asked many times but astoundingly, I have not seen a consistent answer, so I will give it a try myself:
If you have a tableview containing your own custom UITableViewCells that contain UITextViews and UILabels whose height must be determined at runtime, how are you supposed to determine the height for each row in heightForRowAtIndexPath?
The most obvious first idea is to calculate the height for each cell by calculating and then summing the heights of each view inside the cell inside of cellForRowAtIndexPath, and store that final total height for later retrieval.
This will not work however because cellForRowAtIndexPath is called AFTER heightForRowAtIndexPath.
The only thing I can think of is to do all the calculations inside viewDidLoad, create all the UITableViewCells then, calculate the cells height and store that in a custom field inside your UITableViewCell subclass, and put each cell in an NSMutableDictionary with the indexPath as the the key, and then simply retrieve the cell from the dictionary using the indexPath inside cellForRowAtIndexPath and heightForRowAtIndexPath, returning either the custom height value or the cell object itself.
This approach seems wrong though because it does not make use of dequeueReusableCellWithIdentifier, instead I would be loading all the cells at once into a dictionary in my controller, and the delegate methods would be doing nothing more than retrieving the correct cell from the dictionary.
I don't see any other way to do it though. Is this a bad idea - if so, what is the correct way to do this?
This is how I calculate the height of a cell based on the amount of text in a UTextView:
Of course, you would need to adjust the
PADDING
and other variables to fit your needs, but this sets the height of the cell which has aUITextView
in it, based on the amount of text supplied. so if there are only 3 lines of text, the cell is fairly short, where as if there are 14 lines of text, the cell is rather large in height.An approach I have used in the past is to create a class variable to hold a single instance of the cell you are going to be using in the table (I call it a prototype cell). Then in the custom cell class I have a method to populate the data and determine the height the cell needs to be. Note that it can be a simpler variant of the method to really populate the data - instead of actually resizing a UILabel in a cell for example, it can just use the NSString height methods to determine how tall the UILabel would be in the final cell and then use the total cell height (plus a border on the bottom) and UILabel placement to determine the real height. YOu use the prototype cell just to get an idea of where elements are placed so you know what it means when a label is going to be 44 units high.
In
heightForRow:
I then call that method to return the height.In
cellForRow:
I use the method that actually populates labels and resizes them (you never resize the UITableView cell yourself).If you want to get fancy, you can also cache the height for each cell based on the data you pass in (for instance it could just be on one NSString if that's all that determines height). If you have a lot of data that's often the same it may make sense to have a permanent cache instead of just in-memory.
You can also try estimating line count based on character or word count, but in my experience that never works - and when it goes wrong it usually messes up a cell and all the cells below it.
So, I think you can do this without having to create your cells all at once (which, as you suggest, is wasteful and also probably impractical for a large number of cells).
UIKit adds a couple of methods to NSString, you may have missed them as they're not part of the main NSString documentation. The ones of interest to you begin:
Here is the link to the Apple docs.
In theory, these NSString additions exist for this exact problem: to figure out the size that a block of text will take up without needing to load the view itself. You presumably already have access to the text for each cell as part of your table view datasource.
I say 'in theory' because if you're doing formatting in your UITextView your mileage may vary with this solution. But I'm hoping it will get you at least part way there. There's an example of this on Cocoa is My Girlfriend.
The best implementation of this that I've seen is the way the Three20 TTTableView classes do it.
Basically they have a class derived from UITableViewController that delegates the
heightForRowAtIndexPath:
method to a class method on a TTTableCell class.That class then returns the right height, invariably by doing the same sort of layout calculations as you do in the draw methods. By moving it to the class it avoids writing code that depends on the cell instance.
There's really no other option - for performance reasons the framework won't create cells before asking for their heights, and you don't really want to do that either if there could be a lot of rows.