Building Totem 2.0:
Dynamic UITableViewCell Height for Custom Cells in iOS 7

Many iOS apps like Twitter (and my upcoming Secular Totem 2.0 app) use UITableViewControllers to display lists of data. One of the trickier aspects of programming these interfaces is implementing variable height cells based on their content.

Demo

Apple provides some of the functionality to provide this capability, like UILabel’s sizeThatFits method. But that only provides a way for controls to resize, not reposition. And the positioning control by itself is not adequate for the job. And since the cell’s height callback function is called prior to the cell being created, referencing the current cell within that function isn’t possible without creating an infinite callback loop.

So to solve this issue, developers typically create ornate class methods to read in cell data and create composite labels and strings that mimic the behavior of the final output, calculate the dimensions, add in padding, and determine the height. This can be relatively effective, but sometimes a cell will get rendered without enough height or with a bit too much, due to the differences in how a control like a UILabel renders versus a simulation created in code.

I’ve found that the ideal method is to create a single prototype of the custom UITableCell (a property), provide it with the data, tell it to render, and have the cell report back the height it needs. This method renders the same cell object seen on-screen, but behind the scenes, and returns the exact measurements every time. For the sake of this post, I will concentrate on determining the cell height. But this method can also be used to get the width needed.

Create a Custom UITableCell
There are plenty of tutorials out there which describe how to create a custom UITableCell class and XIB file. So I won’t go into that here. Below are my example header and class files for one such custom cell. Keep in mind that the code examples use ARC, of course.

//
// FilterListCell.h
//

import

@interface FilterListCell : UITableViewCell

@property (nonatomic, strong) IBOutlet UILabel *ContentText; @property (nonatomic, strong) IBOutlet UILabel *Subtitle; @property (nonatomic, strong) IBOutlet UILabel *Title;

@property (nonatomic) float requiredCellHeight;

@end

Notice that there are properties (outlets) for the three objects on the cell: Title, Subtitle, and ContentText. They are all UILabel controls. The last is a property which will hold the height required to render the cell. It uses a primitive type (float) so it is merely nonatomic and not a pointer.

//
// FilterListCell.m
//

import "FilterListCell.h"

@interface FilterListCell () @end

@implementation FilterListCell

@synthesize requiredCellHeight;

  • (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];

    if (self) { // Initialization code }

    return self; }

  • (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated];

    // Configure the view for the selected state }

  • (void)layoutSubviews { [super layoutSubviews];

    CGSize maxSize = CGSizeMake(220.0f, CGFLOAT_MAX); CGSize requiredSize = [self.Title sizeThatFits:maxSize]; self.Title.frame = CGRectMake(self.Title.frame.origin.x, self.Title.frame.origin.y, requiredSize.width, requiredSize.height);

    requiredSize = [self.Subtitle sizeThatFits:maxSize]; self.Subtitle.frame = CGRectMake(self.Subtitle.frame.origin.x, self.Subtitle.frame.origin.y, requiredSize.width, requiredSize.height);

    requiredSize = [self.ContentText sizeThatFits:maxSize]; self.ContentText.frame = CGRectMake(self.ContentText.frame.origin.x, self.ContentText.frame.origin.y, requiredSize.width, requiredSize.height);

    // Reposition labels to handle content height changes

    CGRect subtitleFrame = self.Subtitle.frame; subtitleFrame.origin.y = self.Title.frame.origin.y + self.Title.frame.size.height + 3.0f; self.Subtitle.frame = subtitleFrame;

    CGRect contentTextFrame = self.ContentText.frame; contentTextFrame.origin.y = self.Subtitle.frame.origin.y + self.Subtitle.frame.size.height + 7.0f; self.ContentText.frame = contentTextFrame;

    // Calculate cell height

    requiredCellHeight = 15.0f + 3.0f + 7.0f + 15.0f; requiredCellHeight += self.Title.frame.size.height; requiredCellHeight += self.Subtitle.frame.size.height; requiredCellHeight += self.ContentText.frame.size.height;
    }

@end

The process within the cell’s layoutSubviews method is as follows:

  1. Tell each UILabel how to resize on its own (with size restrictions like a fixed width and variable height) once the cell is created and is loaded with content via the outlet properties
  2. Re-position the Subtitle and ContentText (bottom two) labels to accommodate growth in the Title and Subtitle labels
  3. Add up the heights of the labels, along with the padding above, below, and between the labels

FilterListCell

As seen in the image, the fields are all 220 pixels in width. So we use that as a fixed value in our calculation. The height should be boundless, so we specify the constant CGFLOAT_MAX. I’m using 15 as the padding amount at the top and bottom. There is 3 padding between the Title and Subtitle, and 7 padding between the Subtitle and ContentText.

Usage: Create a Prototype Cell
For this example I’m using a XIB with a UIView that has a UISearchBar and UISearchDisplayController. The view is basically blank, and I’m invoking a search with dummy data to render my custom cells in the searchResultsTableView.

ViewController

The header and class file follow.

//
//  SearchViewController.h
//

import

import

import "FilterListCell.h"

@interface SearchViewController : UIViewController

@property (nonatomic) BOOL dismissing; @property (strong, nonatomic) NSString *searchPlaceholderText; @property (weak, nonatomic) IBOutlet UISearchBar *searchBar;

@property (strong) FilterListCell *cellPrototype;

@end

The key in the header is the cellPrototype declaration. That’s our prototype cell to be used for calculating height.

//
//  SearchViewController.m
//

import "SearchViewController.h"

import "FilterListCell.h"

@interface SearchViewController () @end

@implementation SearchViewController

@synthesize searchPlaceholderText; @synthesize dismissing;

  • (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];

    if (self) { dismissing = NO; }

    return self; }

  • (void)viewDidLoad { [super viewDidLoad];

    // Create prototype cell for calculating height static NSString *CellIdentifier = @"FilterListCell"; [self.searchDisplayController.searchResultsTableView registerNib:[UINib nibWithNibName:@"FilterListCell" bundle:nil] forCellReuseIdentifier:CellIdentifier]; self.cellPrototype = [self.searchDisplayController.searchResultsTableView dequeueReusableCellWithIdentifier:CellIdentifier]; }

pragma mark - Table view data source

  • (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 1; }

  • (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return 6; }

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { @try { self.cellPrototype.Title.text = [self getTitle:indexPath]; self.cellPrototype.Subtitle.text = [self getSubtitle:indexPath]; self.cellPrototype.ContentText.text = [self getContent:indexPath]; [self.cellPrototype layoutSubviews];

        return MAX(self.cellPrototype.requiredCellHeight, 88.0f);
}

@catch (NSException *e)
{
        NSLog(@"Exception: %@", e);
    return 100.0f;
}

}

  • (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"FilterListCell"; FilterListCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil) { NSArray *nibObjects = [[NSBundle mainBundle] loadNibNamed:@"FilterListCell" owner:nil options:nil];

    for (id currentObject in nibObjects)
    {
        if ([currentObject isKindOfClass:[FilterListCell class]])
        {
            cell = (FilterListCell *)currentObject;
        }
    }
    

    }

    cell.Title.text = [self getTitle:indexPath]; cell.Subtitle.text = [self getSubtitle:indexPath]; cell.ContentText.text = [self getContent:indexPath];

    return cell; }

  • (NSString *)getContent:(NSIndexPath *)indexPath { NSString *content = [[NSString alloc] init];

    switch (indexPath.row) { case 0: content = @"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt."; break; case 1: content = @"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt."; break; case 2: content = @"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt."; break; case 3: content = @"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt."; break; case 4: content = @"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt."; break; case 5: content = @"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt."; break;

    }

    return content; }

  • (NSString *)getTitle:(NSIndexPath *)indexPath { NSString *title = [[NSString alloc] init];

    switch (indexPath.row) { case 0: title = @"Richard Dawkins"; break; case 1: title = @"Dan Dennett"; break; case 2: title = @"khjdf gkjh dfgkjh dfgkj hdfgkj hdfgkj gkj dgkjd gfkj h"; break; case 3: title = @"khjdf gkjh dfgkjh dfgkj hdfgkj hdfgkj gkj dgkjd gfkj"; break; case 4: title = @"Sam Harris"; break; case 5: title = @"Christopher Hitchens"; break; }

    return title; }

  • (NSString *)getSubtitle:(NSIndexPath *)indexPath { NSString *subtitle = [[NSString alloc] init];

    switch (indexPath.row) { case 0: subtitle = @"The God Delusion"; break; case 1: subtitle = @"khjdf gkjh dfgkjh dfgkj hdfgkj hdfgkj gkj dgkjd gfkj h"; break; case 2: subtitle = @"khjdf gkjh dfgkjh dfgkj hdfgkj hdfgkj gkj dgkjd gfkj h"; break; case 3: subtitle = @"khjdf gkjh dfgkjh dfgkj hdfgkj hdfgkj gkj dgkjd gfkj"; break; case 4: subtitle = @"Letter to a Christian Nation"; break; case 5: subtitle = @"God is Not Great"; break; }

    return subtitle; }

pragma mark - Search

  • (void)enableControlsInView:(UIView *)view { for (id subview in view.subviews) { if ([subview isKindOfClass:[UIControl class]]) { [subview setEnabled:YES]; } [self enableControlsInView:subview]; } }

  • (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { dismissing = YES; [searchBar resignFirstResponder]; [self dismissViewControllerAnimated:NO completion:nil]; }

  • (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar { [self enableControlsInView:self.searchBar]; }

  • (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller { if (dismissing == NO) { [self.searchBar becomeFirstResponder]; } }

@end

There are two key code blocks to notice here. First, within viewDidLoad the prototype cell is created. The second is within the heightForRowAtIndexPath callback function, where the prototype cell is loaded with the same data the real cell will receive in the cellForRowAtIndexPath callback function, and then it is forced to render in memory by calling the layoutSubviews method within the FilterListCell class.

Conclusion
This is a basic example that uses inefficient, separate methods to provide data for the labels, etc. But it’s meant to illustrate the implementation and provide a springboard for your own tests.

I had to turn off comments due to SPAM, so if you have any feedback, please contact me using the contact form.

And best of luck!