When to calculate heightForRowAtIndexPath for a table when each cell height is dynamic?

I asked this question many times, but amazingly, I did not see a consistent answer, so I will try it myself:

If you have a table view containing your own UITableViewCells containing UITextViews and UILabels, whose height should be determined at runtime, how should you 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 cellForRowAtIndexPath and storing that final total height for later retrieval.

This will not work because cellForRowAtIndexPath is called AFTER heightForRowAtIndexPath.

The only thing I can think of is to do all the calculations inside the viewDidLoad, then create all the UITableViewCells, calculate the cell height and save this in a custom field inside your UITableViewCell subclass and put each cell in NSMutableDictionary using indexPath as a key and then just retrieve the cell from the dictionary, using indexPath inside cellForRowAtIndexPath and heightForRowAtIndexPath, returning either a custom height value or the cell object itself.

This approach seems wrong, although due to the fact that it does not use dequeueReusableCellWithIdentifier, instead, I would immediately load all the cells into the dictionary in my controller, and the delegate methods would do nothing more than get the correct cell from the dictionary.

I see no other way to do this, though. This is a bad idea - if so, what is the right way to do this?

+42
iphone cocoa-touch
Jan 27 '11 at 23:29
source share
10 answers

The way Apple implements a UITableView is not intuitive for everyone, and it is easy to understand the meaning of heightForRowAtIndexPath: The main intention is that it is a quick and easy method that can be called for each row of the table quite often. This contrasts with cellForRowAtIndexPath: which is often slower and more intensive than memory, but only called for strings that really need to be displayed at any given time.

Why does Apple implement this? Part of the reason is that it is almost always cheaper (or maybe cheaper if you code it correctly) in order to calculate the height of the line than it should build and fill in the whole cell. Given that in many tables the height of each cell will be the same, it is often much cheaper. And another part of the reason is that iOS needs to know the size of the entire table: this allows you to create scrollbars and customize them as scrollbars, etc.

Thus, if each cell height is not the same, then when a UITableView is created and whenever you send it a reloadData message, the data source is sent to one heightForRowAtIndexPath message for each cell. Therefore, if your table has 30 cells, this message is sent 30 times. Say only six of the 30 cells are visible on the screen. In this case, when creating and sending the reloadData message, the UITableView will send one cellForRowAtIndexPath message to the visible line, that is, this message is sent six times.

Some people sometimes wonder how to calculate cell height without creating the views themselves . But usually it’s easy to do.

For example, if the height of the lines depends on the size, because they contain a different amount of text, you can use one of the sizeWithFont: methods in the corresponding line to perform the calculations. This is faster than creating a view and then measuring the result. Note that if you change the height of the cell, you will need to either reload the entire table (using reloadData - this will ask the delegate for each height, but only asks for visible cells) or selectively reload the rows in which the size is changed (which, in the last the time I checked also calls heightForRowAtIndexPath: on any line, but also does some scrolling for a good estimate).

See this question and perhaps also this one .

+59
Sep 09 '11 at 15:03
source share

So, I think you can do this without creating your cells right away (which you think is wasteful and also probably impractical for a lot of cells).

UIKit adds several methods to NSString, you may have missed them because they are not part of the core NSString documentation. Those that interest you begin:

 - (CGSize)sizeWithFont... 

Here is a link to Apple docs .

In theory, these NSString add-ons exist for this exact problem: figuring out the size a block of text will take without having to load the view itself. Presumably, you already have access to the text for each cell as part of the table data source.

I say “theoretically” because if you do formatting in your UITextView, your mileage may vary depending on this solution. But I hope this helps you at least partially. Here is an example of this on Cocoa - my girlfriend .

+10
Jan 28 '11 at 1:00
source share

The approach I used in the past is to create a class variable to store one instance of the cell that you are going to use in the table (I call it a prototype cell). Then in the user cell class, I have a method to populate the data and determine the height that the cell should be. Note that there may be a simpler version of the method to really populate the data - instead of actually resizing the UILabel in the cell, for example, it can simply use the NSString height methods to determine how tall the UILabel will be in the last cell and then use the total cell height (plus border below) and UILabel placement to determine the real height. You use the prototype cell to get an idea of ​​where the elements are located, so you know what that means when the label is 44 units.

In heightForRow: Then I call this method to return the height.

In cellForRow: I use a method that actually fills the labels and resizes them (you never resize the UITableView cell yourself).

If you want a fantasy, you can also cache the height for each cell based on the data that you are transmitting (for example, it can be only one NSString, if all this determines the height). If you have a lot of data that is often the same, it might make sense to have a constant cache, not just in memory.

You can also try to estimate the number of lines based on the number of characters or words, but in my experience, which never works - and when it goes wrong, it usually messes up the cell and all the cells underneath.

+5
Jan 28 2018-11-11T00:
source share

Here's how I calculate cell height based on the amount of text in a UTextView:

 #define PADDING 21.0f - (CGFloat)tableView:(UITableView *)t heightForRowAtIndexPath:(NSIndexPath *)indexPath { if(indexPath.section == 0 && indexPath.row == 0) { NSString *practiceText = [practiceItem objectForKey:@"Practice"]; CGSize practiceSize = [practiceText sizeWithFont:[UIFont systemFontOfSize:14.0f] constrainedToSize:CGSizeMake(tblPractice.frame.size.width - PADDING * 3, 1000.0f)]; return practiceSize.height + PADDING * 3; } return 72; } 

Of course, you will need to adjust PADDING and other variables to suit your needs, but this sets the height of the cell that has a UITextView based on the amount of text provided. therefore, if there are only 3 lines of text, the cell is quite short, where, as if there were 14 lines of text, the cell was quite large in height.

+5
Sep 08 2018-11-11T00:
source share

The best implementation of this that I have seen is how Three20 TTTableView classes do it.

They basically have a class derived from the UITableViewController that delegates the method heightForRowAtIndexPath: to the class method in the TTTableCell class.

This class returns the correct height, invariably, doing the same layout calculations as in drawing methods. By moving it to a class, it avoids writing code that depends on the cell instance.

Actually there is no other option - for performance reasons, the infrastructure will not create cells before requesting their heights, and you really do not want to do this if there can be many rows.

+3
Sep 07 2018-11-11T00:
source share

The problem with moving the calculation of each cell in tableView: heightForRowAtIndexPath: is that all cells are then recounted each time reloadData is called. The path is too slow, at least for my application, where there may be 100 lines. Here's an alternative solution that uses the default line height and caches line heights when calculating them. When the height is changed or first calculated, a table reload is planned to inform the table view of the new heights. This means that the lines are displayed twice when their heights change, but this is not significant 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 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; } 
+3
Feb 07 2018-12-12T00:
source share

A really good question: look for a deeper understanding of this.

Clarification of the problem:

  • The height for the row is called before (cellForRowAtIndexPath)
  • Most people compute height type information in CELL (cellForRowAtIndexPath).



Some of the solutions are surprisingly simple / effective:

  • Solution 1: get heightForRowAtIndexPath to compute cell specifications. Massimo Kafaro September 9

  • Solution 2: do the first “standard size” pass for cells, cache results when you have cell heights, and then reload the table with the new heights - Symmetric

  • solution 3: another interesting answer seems to be related to the participation of three20s, but based on the answer it seems that there is no cell in the / xib storyboard that would make this “problem” much easier to solve.

+2
Dec 04
source share

I went with the idea that I originally proposed that works fine, and I load all user cells ahead of time in viewDidLoad, storing them in NSMutableDictionary with my index as the key. I post the appropriate code and will love any criticisms or opinions that anyone has about this approach. In particular, I'm not sure if there is a memory leak problem with the way I create UITableViewCells from nib in viewDidLoad - since I don't release them.

 @interface RecentController : UIViewController <UITableViewDelegate, UITableViewDataSource> { NSArray *listData; NSMutableDictionary *cellBank; } @property (nonatomic, retain) NSArray *listData; @property (nonatomic, retain) NSMutableDictionary *cellBank; @end @implementation RecentController @synthesize listData; @synthesize cellBank; --- - (void)viewDidLoad { --- self.cellBank = [[NSMutableDictionary alloc] init]; --- //create question objects… --- NSArray *array = [[NSArray alloc] initWithObjects:question1,question2,question3, nil]; self.listData = array; //Pre load all table row cells int count = 0; for (id question in self.listData) { NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"QuestionHeaderCell" owner:self options:nil]; QuestionHeaderCell *cell; for (id oneObject in nib) { if([oneObject isKindOfClass:[QuestionHeaderCell class]]) cell = (QuestionHeaderCell *) oneObject; NSNumber *key = [NSNumber numberWithInt:count]; [cellBank setObject:[QuestionHeaderCell makeCell:cell fromObject:question] forKey:key]; count++; } } [array release]; [super viewDidLoad]; } #pragma mark - #pragma mark Table View Data Source Methods -(NSInteger) tableView: (UITableView *) tableView numberOfRowsInSection: (NSInteger) section{ return [self.listData count]; } -(UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath{ NSNumber *key = [NSNumber numberWithInt:indexPath.row]; return [cellBank objectForKey:key]; } -(CGFloat) tableView: (UITableView *) tableView heightForRowAtIndexPath: (NSIndexPath *) indexPath{ NSNumber *key = [NSNumber numberWithInt:indexPath.row]; return [[cellBank objectForKey:key] totalCellHeight]; } @end @interface QuestionHeaderCell : UITableViewCell { UITextView *title; UILabel *createdBy; UILabel *category; UILabel *questionText; UILabel *givenBy; UILabel *date; int totalCellHeight; } @property (nonatomic, retain) IBOutlet UITextView *title; @property (nonatomic, retain) IBOutlet UILabel *category; @property (nonatomic, retain) IBOutlet UILabel *questionText; @property (nonatomic, retain) IBOutlet UILabel *createdBy; @property (nonatomic, retain) IBOutlet UILabel *givenBy; @property (nonatomic, retain) IBOutlet UILabel *date; @property int totalCellHeight; +(UITableViewCell *) makeCell:(QuestionHeaderCell *) cell fromObject:(Question *) question; @end @implementation QuestionHeaderCell @synthesize title; @synthesize createdBy; @synthesize givenBy; @synthesize questionText; @synthesize date; @synthesize category; @synthesize totalCellHeight; - (void)dealloc { [title release]; [createdBy release]; [givenBy release]; [category release]; [date release]; [questionText release]; [super dealloc]; } +(UITableViewCell *) makeCell:(QuestionHeaderCell *) cell fromObject:(Question *) question{ NSUInteger currentYpos = 0; cell.title.text = question.title; CGRect frame = cell.title.frame; frame.size.height = cell.title.contentSize.height; cell.title.frame = frame; currentYpos += cell.title.frame.size.height + 2; NSMutableString *tempString = [[NSMutableString alloc] initWithString:question.categoryName]; [tempString appendString:@"/"]; [tempString appendString:question.subCategoryName]; cell.category.text = tempString; frame = cell.category.frame; frame.origin.y = currentYpos; cell.category.frame = frame; currentYpos += cell.category.frame.size.height; [tempString setString:@"Asked by "]; [tempString appendString:question.username]; cell.createdBy.text = tempString; frame = cell.createdBy.frame; frame.origin.y = currentYpos; cell.createdBy.frame = frame; currentYpos += cell.createdBy.frame.size.height; cell.questionText.text = question.text; frame = cell.questionText.frame; frame.origin.y = currentYpos; cell.questionText.frame = frame; currentYpos += cell.questionText.frame.size.height; [tempString setString:@"Advice by "]; [tempString appendString:question.lastNexusUsername]; cell.givenBy.text = tempString; frame = cell.givenBy.frame; frame.origin.y = currentYpos; cell.givenBy.frame = frame; currentYpos += cell.givenBy.frame.size.height; cell.date.text = [[[MortalDataStore sharedInstance] dateFormat] stringFromDate: question.lastOnDeck]; frame = cell.date.frame; frame.origin.y = currentYpos-6; cell.date.frame = frame; currentYpos += cell.date.frame.size.height; //Set the total height of cell to be used in heightForRowAtIndexPath cell.totalCellHeight = currentYpos; [tempString release]; return cell; } @end 
0
Jan 29 2018-11-11T00:
source share

Here's what I do in a very simple case - a cell containing a note on a shortcut. The note itself is limited by the maximum length that I impose, so I use multi-line UILabel, and I dynamically calculate the correct eight for each cell, as shown in the following example. You can deal with a UITextView in much the same way.

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... Note *note = (Note *) [fetchedResultsController objectAtIndexPath:indexPath]; cell.textLabel.text = note.text; cell.textLabel.numberOfLines = 0; // no limits DateTimeHelper *dateTimeHelper = [DateTimeHelper sharedDateTimeHelper]; cell.detailTextLabel.text = [dateTimeHelper mediumStringForDate:note.date]; cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton; return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ //NSLog(@"heightForRowAtIndexPath: Section %d Row %d", indexPath.section, indexPath.row); UITableViewCell *cell = [self tableView: self.tableView cellForRowAtIndexPath: indexPath]; NSString *note = cell.textLabel.text; UIFont *font = [UIFont fontWithName:@"Helvetica" size:14.0]; CGSize constraintSize = CGSizeMake(280.0f, MAXFLOAT); CGSize bounds = [note sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap]; return (CGFloat) cell.bounds.size.height + bounds.height; } 
0
Sep 09 '11 at 10:44
source share

when I searched this topic again and again, finally, this logic came to my mind. simple code, but perhaps not efficient enough, but so far this is the best I can find.

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSDictionary * Object=[[NSDictionary alloc]init]; Object=[Rentals objectAtIndex:indexPath.row]; static NSString *CellIdentifier = @"RentalCell"; RentalCell *cell = (RentalCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier]; } NSString* temp=[Object objectForKey:@"desc"]; int lines= (temp.length/51)+1; //so maybe here, i count how many characters that fit in one line in this case 51 CGRect correctSize=CGRectMake(cell.infoLabel.frame.origin.x, cell.infoLabel.frame.origin.y, cell.infoLabel.frame.size.width, (15*lines)); //15 (for new line height) [cell.infoLabel setFrame:correctSize]; //manage your cell here } 

and here is the rest of the code

 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ NSDictionary * Object=[[NSDictionary alloc]init]; Object=[Rentals objectAtIndex:indexPath.row]; static NSString *CellIdentifier = @"RentalCell"; RentalCell *cells = (RentalCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; NSString* temp=[Object objectForKey:@"desc"]; int lines= temp.length/51; return (CGFloat) cells.bounds.size.height + (13*lines); } 
0
Dec 11 '12 at 7:05
source share



All Articles