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?
The problem with moving the calculation of each cell to tableView:heightForRowAtIndexPath: is that all the cells are then recalculated every time reloadData is called. Way too slow, at least for my application where there may be 100's of rows. Here's an alternative solution that uses a default row height, and caches the row heights when they are calculated. When a height changes, or is first calculated, a table reload is scheduled to inform the table view of the new heights. This does mean that rows are displayed twice when their heights change, but that's minor in comparison:
tableView:heightForRowAtIndexPath:
tableView:cellForRowAtIndexPath:
I went with the idea I originally proposed, which appears to work fine, whereby I load all the custom cells ahead of time in viewDidLoad, store them in a NSMutableDictionary with their index as the key. I am posting the relevant code and would love any critiques or opinions anyone has about this approach. Specifically, I am not sure whether there is any memory leak issue with the way I am creating the UITableViewCells from the nib in viewDidLoad - since I don't release them.
The way Apple implements UITableView is not intuitive to everyone and it's easy to misunderstand the role of
heightForRowAtIndexPath:
. The general intention is that this is a faster and light-on-memory method that can be called for every row in the table quite frequently. This contrasts withcellForRowAtIndexPath:
which is often slower and more memory intensive, but is only called for the rows that are actually need to be displayed at any given time.Why do Apple implement it like this? Part of the reason is that it's almost always cheaper (or can be cheaper if you code it right) to calculate the height of a row than it is to build and populate a whole cell. Given that in many tables the height of every cell will be identical, it is often vastly cheaper. And another part of the reason is because iOS needs to know the size of the whole table: this allows it to create the scroll bars and set it up on a scroll view etc.
So, unless every cell height is the same, then when a UITableView is created and whenever you send it a reloadData message, the datasource is sent one heightForRowAtIndexPath message for each cell. So if your table has 30 cells, that message gets sent 30 times. Say only six of those 30 cells are visible on screen. In that case, when created and when you send it a reloadData message, the UITableView will send one cellForRowAtIndexPath message per visible row, i.e. that message gets sent six times.
Some people are sometimes puzzled about how to calculate a cell height without creating the views themselves. But usually this is easy to do.
For example, if your row heights vary in size because they hold varying amounts of text, you can use one of the
sizeWithFont:
methods on the relevant string to do the calculations. This is quicker than building a view and then measuring the result. Note, that if you change the height of a cell, you will need to either reload the whole table (with reloadData - this will ask the delegate for every height, but only ask for visible cells) OR selectively reload the rows where the size has changed (which, last time I checked, also callsheightForRowAtIndexPath:
on ever row but also does some scrolling work for good measure).See this question and perhaps also this one.
Here is what I do in very simple case, a cell containing a note held in a label. The note itself is constrained to a maximum length I am imposing, so I use a multi-line UILabel and I compute dynamically the correct eight for each cell as shown in the following example. You can deal with an UITextView pretty much the same.
as i searched over and over about this topic, finally this logic came to my thought. a simple code, but maybe not efficient enough, but so far it's the best i can find.
and here is the rest of the code
Really good question: looking for more insight on this as well.
Clarifying the issue:
Some of the solutions are surprisingly simple/effective:
solution 1: force the heightForRowAtIndexPath to calculate the the cell's specs. Massimo Cafaro Sept 9th
solution 2: do a first pass "standard size" for the cells, cache results when you do have cell heights, then reload the table using the new heights - Symmetric
solution 3: the other interesting answer seems to be the involving three20 but based on the answer it seems that there isn't a cell drawn in storyboard/xib which would make this "problem" much easier to solve.