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
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:
@interface MyTableViewController : UITableViewController {
NSMutableDictionary *heightForRowCache;
BOOL reloadRequested;
NSInteger maxElementBottom;
NSInteger minElementTop;
}
tableView:heightForRowAtIndexPath:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// If we've calculated the height for this cell before, get it from the height cache. If
// not, return a default height. The actual size will be calculated by cellForRowAtIndexPath
// when it is called. Do not set too low a default or UITableViewController will request
// too many cells (with cellForRowAtIndexPath). Too high a value will cause reloadData to
// be called more times than needed (as more rows become visible). The best value is an
// average of real cell sizes.
NSNumber *height = [heightForRowCache objectForKey:[NSNumber numberWithInt:indexPath.row]];
if (height != nil) {
return height.floatValue;
}
return 200.0;
}
tableView:cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Get a reusable cell
UITableViewCell *currentCell = [tableView dequeueReusableCellWithIdentifier:_filter.templateName];
if (currentCell == nil) {
currentCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:_filter.templateName];
}
// Configure the cell
// +++ unlisted method sets maxElementBottom & minElementTop +++
[self configureCellElementLayout:currentCell withIndexPath:indexPath];
// Calculate the new cell height
NSNumber *newHeight = [NSNumber numberWithInt:maxElementBottom - minElementTop];
// When the height of a cell changes (or is calculated for the first time) add a
// reloadData request to the event queue. This will cause heightForRowAtIndexPath
// to be called again and inform the table of the new heights (after this refresh
// cycle is complete since it's already been called for the current one). (Calling
// reloadData directly can work, but causes a reload for each new height)
NSNumber *key = [NSNumber numberWithInt:indexPath.row];
NSNumber *oldHeight = [heightForRowCache objectForKey:key];
if (oldHeight == nil || newHeight.intValue != oldHeight.intValue) {
if (!reloadRequested) {
[self.tableView performSelector:@selector(reloadData) withObject:nil afterDelay:0];
reloadRequested = TRUE;
}
}
// Save the new height in the cache
[heightForRowCache setObject:newHeight forKey:key];
NSLog(@"cellForRow: %@ height=%@ >> %@", indexPath, oldHeight, newHeight);
return currentCell;
}