22

My app needs to have variable height table cells (as in each table cell differs in height, not that each cell needs to be able to resize itself).

I have a solution that currently works, but it's kludgy and slow.

My Current Solution:

Before the table cells are rendered, I calculate how high each cell needs to be by calling sizing methods such as -sizeWithFont:constrainedToSize: on its data. I then add up the heights, allow for some padding and store the result with the data.

Then when my UITableViewDelegate receives the -tableview:heightForRowAtIndexPath: I work out which item will be rendered for that cell and return the height that I calculated previously.

As I said, this works, but calling -sizeWithFont:constrainedToSize: is very slow when you're doing it for hundreds of items sequentially, and I feel it can be done better.

So for this to work, I had to maintain two parts of code - one that would calculate the cell heights, and one that would actually draw the cells when the time comes.

If anything about the model item changed, I had to update both of these chunks of code, and now and again they still don't even match up perfectly, sometimes resulting in table cells that are slightly too small for a given item, or too large.

My Proposed Solution:

So I want to do away with the precalculating the cell height. A) because it breaks the MVC paradigm and B) because it's slow.

So my cell draws itself, and as a result, ends up with the correct cell height. My problem is that I have no way of telling the table view the height of the cell before its drawn - by which time its too late.

I tried calling -cellForRowAtIndexPath: from within -tableView:heightForRowAtIndexPath: but this gets stuck in an infinite loop, since the first calls the second at some point, and vice versa (at least this is what I saw when I tried it).

So that option is out of the question.

If I don't specify a size in the height for row delegate method, then the table view goes screwwy. The cells are the perfect height, but their x position is that of cells of fixed heights.

Messed Table Cells http://jamsoftonline.com/images/messed_table_cells.png

Notice how the bottom cell is the correct size - it's just overlapping the previous cell, and the previous cell overlaps its previous, and so on and so forth.

Also using this method, while scrolling there is some artifacting occurring which I think may be related to the reuse identifier for the cells.

So any help here would be gratefully appreciated.

Jasarien
  • 58,279
  • 31
  • 157
  • 188
  • I've the same problem. -cellForRowAtIndexPath: calls -tableView:heightForRowAtIndexPath: if called in the latter(notice it does not call the latter if not called in the latter) make me crazy. – an0 Oct 21 '09 at 07:56

3 Answers3

38

Here's what I use. NSString has a method that will tell you the dimensions of a textbox based on the font information and the height/width constraints you give it.

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *text = [self getTextForIndexPath:indexPath];
    UIFont *font = [UIFont systemFontOfSize:14];
    CGSize size = [self getSizeOfText:text withFont:font];

    return (size.height + 11); // I put some padding on it.
}

Then you write a method pull the text for this cell...

- (NSString *)getTextForIndexPath:(NSIndexPath *)indexPath
{
    NSString *sectionHeader = [self.tableSections objectAtIndex:[indexPath section]];
    NSString *sectionContent = [self.tableData objectForKey:sectionHeader];

    return sectionContent;
}

And this is to get the size of the text.

- (CGSize)getSizeOfText:(NSString *)text withFont:(UIFont *)font
{
    return [text sizeWithFont:font constrainedToSize:CGSizeMake(280, 500)];
}
Adam Carter
  • 4,741
  • 5
  • 42
  • 103
JLeonard
  • 8,520
  • 7
  • 36
  • 36
  • 1
    How can I use this method for only one cell in my grouped table View? I'd like to return the height of the cell based on the height of a textView inside the cell... – matteodv Sep 01 '10 at 09:47
  • Hi. I want to know what is the `self.tableSections` and `self.tableData`. I know that they are NSArray and NSDictionary, but does this mean that you made an array of table sections and a dictionary for the content of these sections? I am asking because I only use one section and I can't seem to make it work. – Anna Fortuna Sep 28 '12 at 03:46
2

Just a thought:

  • What if you had, say, six different types of cells each with their own identifier and a fixed height. One would be for a single-line cell, the other for a two-line cell, etc...

  • Every time your model changes, calculate the height for that row then find the nearest cell type that has height closest to what you need. Save that celltype identifier with the model. You can also store the fixed row height for that cell in the model so you can return it in the tableview:heightForRowAtIndexPath call (I wouldn't get too hung up on forcing it to calculate inside the cell class itself--technically it's not part of the cell drawing functionality and more something the tableview uses to decide which cell type to create).

  • At runtime, when asked to return a cell for that row all you need to do is create (or obtain from the cell cache) a cell with the celltype identifier, load the values and you're good to go.

If the cell height calculation is too slow, then you could pull the same trick the tableview cache does and do it only on-demand when the cell comes into view. At any given time, you would only have to do it for the cells in view, and then only for a single cell as it scrolls into view at either end.

Ramin
  • 13,343
  • 3
  • 33
  • 35
  • I've toyed with this idea in the past and couldn't get it to do exactly what I wanted. But maybe I can try again. – Jasarien Sep 18 '09 at 10:01
0

I realise this won't work for you due to the infinite loop you mention, but I've had some success with calling the cells layoutSubViews method

Though this may be a little inefficient due to multiple calls to both cellForRowAtIndexPath and layoutSubViews, I find the code is cleaner.

-(float)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyCell *cell = (MyCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath];

    [cell layoutSubviews];

    return CGRectGetHeight(cell.frame);
}

And in the layout code:

- (void)layoutSubviews
{
    [super layoutSubviews];

    //First expand the label to a large height to so sizeToFit isn't constrained
    [self.myArbitrarilyLengthLabel setFrame:CGRectMake(self.myArbitrarilyLengthLabel.frame.origin.x,
                                          self.myArbitrarilyLengthLabel.frame.origin.y,
                                          self.myArbitrarilyLengthLabel.frame.size.width,
                                          1000)];


    //let sizeToFit do its magic
    [self.myArbitrarilyLengthLabel sizeToFit];

    //resize the cell to encompass the newly expanded label
    [self setFrame:CGRectMake(self.frame.origin.x,
                             self.frame.origin.y,
                             self.frame.size.width,
                              CGRectGetMaxY(self.myArbitrarilyLengthLabel.frame) + 10)];
}
chickeninabiscuit
  • 9,061
  • 12
  • 50
  • 56