60

Case

Normally you would use the cellForRowAtIndexPath delegate method to setup your cell. The information set for the cell is important for how the cell is drawn and what the size will be.

Unfortunatly the heightForRowAtIndexPath delegate method is called before the cellForRowAtIndexPath delegate method so we can't simply tell the delegate to return the height of the cell, since this will be zero at that time.

So we need to calculate the size before the cell is drawn in the table. Luckily there is a method that does just that, sizeWithFont, which belongs to the NSString class. However there is problem, in order to calculate the correct size dynamically it needs to know how the elements in the cell will be presented. I will make this clear in an example:

Imagine a UITableViewCell, which contains a label named textLabel. Within the cellForRowAtIndexPath delegate method we place textLabel.numberOfLines = 0, which basically tells the label it can have as many lines as it needs to present the text for a specific width. The problem occurs if we give textLabel a text larger then the width originally given to textLabel. The second line will appear, but the height of the cell will not be automatically adjusted and so we get a messed up looking table view.

As said earlier, we can use sizeWithFont to calculate the height, but it needs to know which Font is used, for what width, etc. If, for simplicity reasons, we just care about the width, we could hardcode that the width would be around 320.0 (not taking padding in consideration). But what would happen if we used UITableViewStyleGrouped instead of plain the width would then be around 300.0 and the cell would again be messed up. Or what happends if we swap from portrait to landscape, we have much more space, yet it won't be used since we hardcoded 300.0.

This is the case in which at some point you have to ask yourself the question how much can you avoid hardcoding.

My Own Thoughts

You could call the cellForRowAtIndexPath method that belongs to the UITableView class to get the cell for a certain section and row. I read a couple of posts that said you don't want to do that, but I don't really understand that. Yes, I agree it will already allocate the cell, but the heightForRowAtIndexPath delegate method is only called for the cells that will be visible so the cell will be allocated anyway. If you properly use the dequeueReusableCellWithIdentifier the cell will not be allocated again in the cellForRowAtIndexPath method, instead a pointer is used and the properties are just adjusted. Then what's the problem?

Note that the cell is NOT drawn within the cellForRowAtIndexPath delegate method, when the table view cell becomes visible the script will call the setNeedDisplay method on the UITableVieCell which triggers the drawRect method to draw the cell. So calling the cellForRowAtIndexPath delegate directly will not lose performance because it needs to be drawn twice.

Okay so by calling the cellForRowAtIndexPath delegate method within the heightForRowAtIndexPath delegate method we receive all the information we need about the cell to determine it's size.

Perhaps you can create your own sizeForCell method that runs through all the options, what if the cell is in Value1 style, or Value2, etc.

Conclusion/Question

It's just a theory I described in my thoughts, I would like to know if what I wrote is correct. Or that maybe there is another way to accomplish the same thing. Note that I want to be able to do things as flexible as possible.

tshepang
  • 12,111
  • 21
  • 91
  • 136
Mark
  • 16,906
  • 20
  • 84
  • 117

7 Answers7

26

Yes, I agree it will already allocate the cell, but the heightForRowAtIndexPath delegate method is only called for the cells that will be visible so the cell will be allocated anyway.

This is incorrect. The table view needs to call heightForRowAtIndexPath (if it's implemented) for all rows that are in the table view, not just the ones currently being displayed. The reason is that it needs to figure out its total height to display the correct scroll indicators.

omz
  • 53,243
  • 5
  • 129
  • 141
  • 1
    If you also implement: `override func tableView(tableView: UITableView!, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat` I think this handles the performance optimization for tables with lots of cells of dynamic heights. Right? – Michael Moser Aug 04 '14 at 20:03
8

I used to do this by:

  1. Creating a collection objects (array of size information (dictionary, NSNumber of row heights, etc.) based on the collection objects that will be used for the table view.

  2. This is done when we're processing the data either from a local or remote source.

  3. I predetermine the type and size of the font that will be used, when I'm creating this collection objects. You can even store the UIFont objects or whatever custom objects used to represent the content.

  4. These collection objects will be used every time I implement UITableViewDataSource or UITableViewDelegate protocols to determine the sizes of the UITableViewCell instances and its subviews, etc.

By doing it this way you can avoid having to subclass UITableViewCell just to get the various size properties of its content.

Don't use an absolute value for initializing the frames. Use a relative value based on the current orientation and bounds.

If we rotate it to any orientation, just do a resizing mechanism at runtime. Make sure the autoresizingMask is set correctly.

You only need the heights, you don't need all of that unnecessary things inside a UITableViewCell to determine the row height. You may not even need the width, because as I said the width value should be relative to the view bounds.

Jesse Armand
  • 1,842
  • 3
  • 17
  • 26
  • "Don't complicate things by considering whether it's grouped or not at runtime. You can query the width of the current table view bounds initially." This is incorrect. The table bounds (and frame) are the same for both plain and grouped style tables. However, the cells in those tables do not have the same widths. The OP is correct that a cell in a plain table might be 320px wide, while a cell in a grouped table might be 300px. – Dan Reese Jun 06 '12 at 19:14
  • I should clarify that I'm not recommending hard-coded values. Just trying to figure out a way to determine cell width so that calculating the height of a dynamic multi-line label isn't so messy. – Dan Reese Jun 06 '12 at 19:24
7

Here is my approach for solving this

  1. I assume in this solution that only one Label has a "dynamic" height
  2. I also assume if we make the label auto size to stretch the height as the cell grows only the cell height is needed to change
  3. I assume that the nib has the appropriate spacing for where the label will be and how much space is above and bellow it
  4. We dont want to change the code every time we change the font or position of the label in the nib

How to update the height:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    // We want the UIFont to be the same as what is in the nib,
    //  but we dont want to call tableView dequeue a bunch because its slow. 
    //  If we make the font static and only load it once we can reuse it every
    //  time we get into this method
    static UIFont* dynamicTextFont;
    static CGRect textFrame;
    static CGFloat extraHeight;
    if( !dynamicTextFont ) {
        DetailCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
        dynamicTextFont = cell.resizeLabel.font;
        CGRect cellFrame = cell.frame;
        textFrame = cell.resizeLabel.frame;
        extraHeight = cellFrame.size.height-textFrame.size.height; // The space above and below the growing field
    }
    NSString* text = .... // Get this from the some object using indexPath 

    CGSize  size = [text sizeWithFont:dynamicTextFont constrainedToSize:CGSizeMake(textFrame.size.width, 200000.f) lineBreakMode:UILineBreakModeWordWrap];

    return size.height+extraHeight;
}

Issues:

  • If you are not using a prototype cell you will need to check if the cell is nil and init it
  • Your nib / storyboard must have the UILabel autosize and have multi line set to 0
Eric
  • 7,787
  • 5
  • 19
  • 34
  • I'm re-using your static font approach for cellforatrowatindex and seeing some improvements in cell recycling. – johndpope Oct 14 '14 at 00:02
4

You should have a look at TTTableItemCell.m in the Three20 framework. It follows a different approach, basically by having each cell class (with some predefined settings like font, layout etc.) implement a shared method + tableView: sizeForItem: (or something like that), where it gets passed the text in the item object. When you look up the text for a specific cell, you can as well look up the appropriate font, too.

Regarding the cell height: You can check your tableView's width and, if necessary, subtract the margins by UITableViewStyleGrouped and the width an eventual index bar and disclosure item (which you look for in the data storage for your cells' data). When the width of the tableView changes, e.g. by interface rotation, you have to call [tableView reloadData].

MrMage
  • 7,282
  • 2
  • 41
  • 71
  • 2
    I will have a look at the TTTableItemCell, for the time being I solved it by subclassing the UITableViewCell and expanding it with - (void)sizeToFitWithTableView:(UITableView *)tableView cellStyle:(UITableViewCellStyle)cellStyle Now when I create the cell I call the method [cell sizeToFitWithTableView:tableView cellStyle:UITableViewCellStyleDefault]; and it sets the frame of all views ImageView, ContentView, TextLabel, DetailTextLabel and AccessoryType and the frame of the Cell of course. Now in heightForRow I can simply retrieve the cell and say cell.frame.size.height (fixed :D) – Mark Feb 06 '10 at 14:47
  • 1
    I do something similar where I have a static instance of the cell which I bind to the data I need to display in the measure call. – Florian Doyon Jan 10 '13 at 23:24
3

I have an idea about dynamic cell height.

Just create one instance of your custom cell as member variable of UITableViewController. In the tableView:heightForRowAtIndexPath: method set the cell's content and return the cell's height.

This way you won't be creating/autoreleasing cell multiple times as you will if you call cellForRowAtIndexPath inside the heightForRowAtIndexPath method.

UPD: For convenience, you can also create a static method in your custom cell class that will create a singleton cell instance for height calculation, set the cell's content and then return it's height.

tableView:heightForRowAtIndexPath: function body will now look like this:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return [MyCell cellHeightForContent:yourContent];
}
tonytony
  • 1,994
  • 3
  • 20
  • 27
  • This has no comments or likes but seems like a great solution for my project. I'll try it and feedback when I'm done! – owencm Jul 29 '13 at 02:43
  • @tonytony Ive used this method for ages, but Ive recently encountered a problem where the calculation takes too long and the app crashes with the error: UICollectionView recieved layout attributes for a cell with an index path that does not exist. Any suggestions? – AlexanderN Nov 21 '13 at 15:28
  • @AlexanderLongbeach I wish I could help you with the issue, but I didn't try using this method with collection views. I'll post here if I happen to encounter the same problem. – tonytony Nov 26 '13 at 20:49
3

To answer the question the original poster asked which was 'is it ok to call cellForRowAtIndexPath?', it's not. That will give you a cell but it will NOT allocate it to that indexPath internally nor will it be re-queued (no method to put it back), so you'll just lose it. I suppose it will be in an autorelease pool and will be deallocated eventually, but you'll still be creating loads of cells over and over again and that is really pretty wasteful.

You can do dynamic cell heights, you can even make them look quite nice, but it's a lot of work to really make them look seamless, even more if you want to support multiple orientations etc.

Rols
  • 31
  • 1
1

Here's my solution which I've used to implement some rather slick cells for a chatting app.

Up to this point I've always been really really irritated with heightForCellAtIndexPath: because it leads to violating the DRY principle. With this solution my heightForRowAtIndexPath: costs 1.5ms per cell which I could shave down to ~1ms.

Basically, you want each subview inside your cell to implement sizeThatFits: Create an offscreen cell which you configure then query the root view with sizeThatFits:CGSizeMake(tableViewWidth, CGFLOAT_MAX).

There are a few gotchas along the way. Some UIKit views have expensive setter operations. For example -[UITextView setText] does a lot of work. The trick here is to create a subclass, buffer the variable, then override setNeedsDisplay to call -[super setText:] when the view is about to be rendered. Of course, you'll have to implement your own sizeThatFits: using the UIKit extensions.

lorean
  • 2,150
  • 19
  • 25