22

When using "Dynamic Prototypes" for specifying UITableView content on the storyboard, there is a "Row Height" property that can be set to Custom.

When instantiating cells, this custom row height is not taken into account. This makes sense, since which prototype cell I use is decided by my application code at the time when the cell is to be instantiated. To instantiate all cells when calculating layout would introduce a performance penalty, so I understand why that cannot be done.

The question then, can I somehow retrieve the height given a cell reuse identifier, e.g.

[myTableView heightForCellWithReuseIdentifier:@"MyCellPrototype"];

or something along that line? Or do I have to duplicate the explicit row heights in my application code, with the maintenance burden that follows?

Solved, with the help of @TimothyMoose:

The heights are stored in the cells themselves, which means the only way of getting the heights is to instantiate the prototypes. One way of doing this is to pre-dequeue the cells outside of the normal cell callback method. Here is my small POC, which works:

#import "ViewController.h"

@interface ViewController () {
    NSDictionary* heights;
}
@end

@implementation ViewController

- (NSString*) _reusableIdentifierForIndexPath:(NSIndexPath *)indexPath
{
    return [NSString stringWithFormat:@"C%d", indexPath.row];
}

- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if(!heights) {
        NSMutableDictionary* hts = [NSMutableDictionary dictionary];
        for(NSString* reusableIdentifier in [NSArray arrayWithObjects:@"C0", @"C1", @"C2", nil]) {
            CGFloat height = [[tableView dequeueReusableCellWithIdentifier:reusableIdentifier] bounds].size.height;
            hts[reusableIdentifier] = [NSNumber numberWithFloat:height];
        }
        heights = [hts copy];
    }
    NSString* prototype = [self _reusableIdentifierForIndexPath:indexPath];
    return [heights[prototype] floatValue];
}

- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 3;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString* prototype = [self _reusableIdentifierForIndexPath:indexPath];
    UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:prototype];
    return cell;
}

@end
Krumelur
  • 27,311
  • 6
  • 71
  • 108
  • I had been using this method for a while, then on a new storyboard, I was getting mostly zero-height (and width) dimensions for the cells. Even the non-zero ones were unexpected values. Disabling size classes restored this functionality. Disappointing, though, since this "fix" disabled important functionality in the storyboard. However, this worked for my immediate need. – jpb Jun 30 '15 at 15:54
  • I found a more complete solution for [solving this problem with size classes enabled](http://stackoverflow.com/questions/27735341/offscreen-uitableviewcells-for-size-calculations-not-respecting-size-class). – jpb Jun 30 '15 at 16:37

2 Answers2

28

For static (non-data-driven) height, you can just dequeue the cell once and store the height:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSNumber *height;
    if (!height) {
        UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"MyCustomCell"];
        height = @(cell.bounds.size.height);
    }
    return [height floatValue];
}

For dynamic (data-driven) height, you can store a prototype cell in the view controller and add a method to the cell's class that calculates the height, taking into account the default content of the prototype instance, such as subview placement, fonts, etc.:

- (MyCustomCell *)prototypeCell
{
    if (!_prototypeCell) {
        _prototypeCell = [self.tableView dequeueReusableCellWithIdentifier:@"MyCustomCell"];
    }
    return _prototypeCell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Data for the cell, e.g. text for label
    id myData = [self myDataForIndexPath:indexPath];

    // Prototype knows how to calculate its height for the given data
    return [self.prototypeCell myHeightForData:myData];
}

Of course, if you're using custom height, you probably have multiple cell prototypes, so you'd store them in a dictionary or something.

As far as I can tell, the table view doesn't attempt to reuse the prototype, presumably because it was dequeued outside of cellForRowAtIndexPath:. This approach has worked very well for us because it allows the designer to modify cells layouts in the storyboard without requiring any code changes.

Edit: clarified the meaning of sample code and added an example for the case of static height.

Timothy Moose
  • 9,773
  • 3
  • 31
  • 44
  • I don't recognize the `heightForData:` method. What does it do? – Krumelur Jan 03 '13 at 21:40
  • `heightForData:` is a method you'd write to calculate the height for the given cell data. – Timothy Moose Jan 04 '13 at 02:13
  • I see. The problem is, I want to use the height stored in the storyboard file itself (a static height). Your pre-dequeueing of cells may work for that too though. I will try it out. – Krumelur Jan 05 '13 at 13:19
  • It works. Thanks! I will update the question with the complete solution to help others. – Krumelur Jan 05 '13 at 13:32
  • 1
    Take a look at [TLIndexPathTools](https://github.com/wtmoose/TLIndexPathTools). It calculates cell heights by automatically creating prototype and a) returning the prototypes static height or, b) if the cell implements the `TLDynamicSizeView` prototype, returning the dynamic height. See the [Dynamic Height](https://github.com/wtmoose/TLIndexPathTools/blob/master/Examples/Dynamic%20Height/Dynamic%20Height/DynamicHeightTableViewController.m) sample project. This is really a secondary feature of the library, but it's worth checking out. – Timothy Moose Jun 18 '13 at 12:28
  • 1
    Problem with this approach is that it doesn't work when your interface orientation changes from Portrait to Landscape. Possible solution: Reset the frame/width of the cell to table's width, before calculating the height in heightForRowAtIndexPath: method. Hope this helps. – Mustafa Aug 22 '14 at 09:08
5

I created a category for UITableView some time ago that may come helpful for this. It stores 'prototype' cells using asociated objects for reusing the prototypes and provides a convenience method for obtaining the height of the row assigned in storyboard. The prototypes are released when the table view is deallocated.

UITableView+PrototypeCells.h

#import <UIKit/UIKit.h>

@interface UITableView (PrototypeCells)

- (CGFloat)heightForRowWithReuseIdentifier:(NSString*)reuseIdentifier;
- (UITableViewCell*)prototypeCellWithReuseIdentifier:(NSString*)reuseIdentifier;

@end

UITableView+PrototypeCells.m

#import "UITableView+PrototypeCells.h"
#import <objc/runtime.h>

static char const * const key = "prototypeCells";

@implementation UITableView (PrototypeCells)
- (void)setPrototypeCells:(NSMutableDictionary *)prototypeCells {
    objc_setAssociatedObject(self, key, prototypeCells, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSMutableDictionary *)prototypeCells {
    return objc_getAssociatedObject(self, key);
}

- (CGFloat)heightForRowWithReuseIdentifier:(NSString*)reuseIdentifier {
    return [self prototypeCellWithReuseIdentifier:reuseIdentifier].frame.size.height;
}

- (UITableViewCell*)prototypeCellWithReuseIdentifier:(NSString*)reuseIdentifier {
    if (self.prototypeCells == nil) {
        self.prototypeCells = [[NSMutableDictionary alloc] init];
    }

    UITableViewCell* cell = self.prototypeCells[reuseIdentifier];
    if (cell == nil) {
        cell = [self dequeueReusableCellWithIdentifier:reuseIdentifier];
        self.prototypeCells[reuseIdentifier] = cell;
    }
    return cell;
}

@end

Usage

Obtaining the static height set in storyboard is as simple as this:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [tableView heightForRowWithReuseIdentifier:@"cellIdentifier"];
}

Assuming a multi-section table view:

enum {
    kFirstSection = 0,
    kSecondSection
};

static NSString* const kFirstSectionRowId = @"section1Id";
static NSString* const kSecondSectionRowId = @"section2Id";

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat height = tableView.rowHeight; // Default UITableView row height
    switch (indexPath.section) {
        case kFirstSection:
            height = [tableView heightForRowWithReuseIdentifier:kFirstSectionRowId];
            break;
        case kSecondSection:
            height = [tableView heightForRowWithReuseIdentifier:kSecondSectionRowId];
    }
    return height;
}

And finally if the row height is dynamic:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    id thisRowData = self.allData[indexPath.row]; // Obtain the data for this row

    // Obtain the prototype cell
    MyTableViewCell* cell = (MyTableViewCell*)[self prototypeCellWithReuseIdentifier:@"cellIdentifier"];

    // Ask the prototype cell for its own height when showing the specified data
    return [cell heightForData:thisRowData];
}
redent84
  • 17,763
  • 4
  • 53
  • 83