Freelance iOS Developer, Rapidly Aging Punk

Auto-Resizing UITableViewCells in iOS8 Without Losing Your Mind

Sep 23, 2014 at 05:33PM

The auto-resizing UITableViewCell has been more of a myth than a reality. I've seen it deployed in apps but for the life of iOS7 I considered it my personal Auto Layout white whale. I estimated my row heights. I calculated my row heights. My row heights were never, ever correct. So I gave up.

iOS8 has promised to make this flummoxing process "easier" than its predecessor. There's even a WWDC session (What's New in Table and Collection Views) to show you the way. I watched the video, I read the documentation, and I still had a buttload of trouble getting auto-resizing to work. But I did eventually get it to work and I'll show you how, nice and slowly, below.

A Note on Using Interface Builder

Don't. I know writing constraints in code is a pain in the ass but I've whittled them down here to be as simple as I possibly could make them, using only the almost-human constraintsWithVisualFormat: method.

Our UITableViewCell Subclass: MagicTableViewCell

I have a specific need for auto-resizing UITableViewCells: The bookmark cells in Pinline. I need to display three pieces of information in each cell, two of which would benefit from being resizable:

So I need a cell with three UILabels. I want each of them to expand horizontally to almost the width of the cell. I want them to be stacked, with a little distance between them, and I want the title and tags labels to expand vertically to fit their content. I'll use a single method to fill in these labels with a Bookmark object that just contains the three strings I need as properties.

Here's our MagicTableViewCell.h:

#import <UIKit/UIKit.h>
#import "Bookmark.h"

@interface MagicTableViewCell : UITableViewCell

-(void)populateWithBookmark:(Bookmark *)bookmark;

@end

And here's our MagicTableViewCell.m:

#import "MagicTableViewCell.h"

@interface MagicTableViewCell ()

@property (strong, nonatomic) UILabel *titleLabel;
@property (strong, nonatomic) UILabel *tagsLabel;
@property (strong, nonatomic) UILabel *urlLabel;

@end

@implementation MagicTableViewCell

+(BOOL)requiresConstraintBasedLayout
{
    return YES;
}

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

    if (self) {
        _titleLabel = [[UILabel alloc] initWithFrame:CGRectInset(self.bounds, 15.0, 0.0)];
        _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
        _titleLabel.numberOfLines = 0;
        _titleLabel.backgroundColor = [UIColor colorWithRed:0.284 green:0.384 blue:0.677 alpha:1.000];
        _titleLabel.translatesAutoresizingMaskIntoConstraints = NO;

        _urlLabel = [[UILabel alloc] initWithFrame:CGRectInset(self.bounds, 15.0, 0.0)];
        _urlLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
        _urlLabel.numberOfLines = 1;
        _urlLabel.backgroundColor = [UIColor colorWithRed:0.940 green:0.654 blue:0.256 alpha:1.000];
        _urlLabel.translatesAutoresizingMaskIntoConstraints = NO;

        _tagsLabel = [[UILabel alloc] initWithFrame:CGRectInset(self.bounds, 15.0, 0.0)];
        _tagsLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
        _tagsLabel.numberOfLines = 0;
        _tagsLabel.backgroundColor = [UIColor colorWithRed:0.973 green:0.824 blue:0.239 alpha:1.000];
        _tagsLabel.translatesAutoresizingMaskIntoConstraints = NO;

        [self.contentView addSubview:_titleLabel];
        [self.contentView addSubview:_urlLabel];
        [self.contentView addSubview:_tagsLabel];
    }

    return self;
}

-(void)updateConstraints
{
    NSMutableArray *constraints = [NSMutableArray new];
    NSDictionary *views = NSDictionaryOfVariableBindings(_titleLabel, _urlLabel ,_tagsLabel);

    [constraints addObjectsFromArray:
     [NSLayoutConstraint
          constraintsWithVisualFormat:@"H:|-15-[_titleLabel]-15-|"
                              options:0
                              metrics:nil
                                views:views]];

    [constraints addObjectsFromArray:
     [NSLayoutConstraint
          constraintsWithVisualFormat:@"H:|-15-[_urlLabel]-15-|"
                              options:0
                              metrics:nil
                                views:views]];

    [constraints addObjectsFromArray:
     [NSLayoutConstraint
          constraintsWithVisualFormat:@"H:|-15-[_tagsLabel]-15-|"
                              options:0
                              metrics:nil
                                views:views]];

    [constraints addObjectsFromArray:
     [NSLayoutConstraint
          constraintsWithVisualFormat:@"V:|-[_titleLabel]-[_urlLabel]-[_tagsLabel]-|"
                              options:0
                              metrics:nil
                                views:views]];

    [self.contentView addConstraints:constraints];
    [super updateConstraints];
}

-(void)populateWithBookmark:(Bookmark *)bookmark
{
    _titleLabel.text = bookmark.title;
    _urlLabel.text = bookmark.url;
    _tagsLabel.text = bookmark.tags;
}

@end

Blow-by-Blow

First up we create three properties for our labels. I like to put these in the .m so that I can be sure they're only populated from methods within the class. Pretty standard stuff:

@interface MagicTableViewCell ()

@property (strong, nonatomic) UILabel *titleLabel;
@property (strong, nonatomic) UILabel *tagsLabel;
@property (strong, nonatomic) UILabel *urlLabel;

@end

Next, in our implementation, we override this crucial class method:

@implementation MagicTableViewCell

+(BOOL)requiresConstraintBasedLayout
{
    return YES;
}

This tells our app that we'll be applying our own constraints for the content view of the cell later on in the updateConstraints method. This is an important piece of Auto Layout magic and forgetting to override this is easy.

Now we override our initializer to include the code that creates our labels and adds them to the cell:

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

    if (self) {
        _titleLabel = [[UILabel alloc] init];
        _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
        _titleLabel.numberOfLines = 0;
        _titleLabel.backgroundColor = [UIColor colorWithRed:0.284 green:0.384 blue:0.677 alpha:1.000];
        _titleLabel.translatesAutoresizingMaskIntoConstraints = NO;

        _urlLabel = [[UILabel alloc] init];
        _urlLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
        _urlLabel.numberOfLines = 1;
        _urlLabel.backgroundColor = [UIColor colorWithRed:0.940 green:0.654 blue:0.256 alpha:1.000];
        _urlLabel.translatesAutoresizingMaskIntoConstraints = NO;

        _tagsLabel = [[UILabel alloc] init];
        _tagsLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
        _tagsLabel.numberOfLines = 0;
        _tagsLabel.backgroundColor = [UIColor colorWithRed:0.973 green:0.824 blue:0.239 alpha:1.000];
        _tagsLabel.translatesAutoresizingMaskIntoConstraints = NO;

        [self.contentView addSubview:_titleLabel];
        [self.contentView addSubview:_urlLabel];
        [self.contentView addSubview:_tagsLabel];
    }

    return self;
}

Some important points to note here:

Next up we lay out our constraints. NSLayoutConstraints are confusing. A great way to get started using them is the constraintsWithVisualFormat: method, which takes a formatted string that kind-of-sort-of looks like the layout you're trying to achieve and then spits out constraints to describe that layout.

First, we create an NSMutableArray to hold the constraints we'll be making. Then we create an NSDictionary of our views using the incredibly convenient NSDictionaryOfVariableBindings() macro:

-(void)updateConstraints
{
    NSMutableArray *constraints = [NSMutableArray new];
    NSDictionary *views = NSDictionaryOfVariableBindings(_titleLabel, _urlLabel, _tagsLabel);

On to the constraints themselves. First we want to set constraints that put each of the labels 15 points away from the left and right edges of the cell's content view. The string you give to constraintsWithVisualFormat: for that looks like this: @"H:|-15-[_theNameOfMyView]-15-|". The H: means that we're working horizontally. The | characters represent the superview, in this case, the left and right edges. The -15- means "a distance of 15 points." Finally, [_theNameOfMyView] represents our view. When you put it all together, @"H:|-15-[_theNameOfMyView]-15-|" means "my view should always be fifteen points away from its superview on its left and right edges."

We want this constraint on all three of our labels:

    [constraints addObjectsFromArray:
     [NSLayoutConstraint
          constraintsWithVisualFormat:@"H:|-15-[_titleLabel]-15-|"
                              options:0
                              metrics:nil
                                views:views]];

    [constraints addObjectsFromArray:
     [NSLayoutConstraint
          constraintsWithVisualFormat:@"H:|-15-[_urlLabel]-15-|"
                              options:0
                              metrics:nil
                                views:views]];

    [constraints addObjectsFromArray:
     [NSLayoutConstraint
          constraintsWithVisualFormat:@"H:|-15-[_tagsLabel]-15-|"
                              options:0
                              metrics:nil
                                views:views]];

Now we want to lay out our labels vertically. This can all be knocked out in one line: @"V:|-[_titleLabel]-[_urlLabel]-[_tagsLabel]-|". This time we're working vertically, so we use V:. The | again represents our superview, this time the top and bottom edges. The views are in brackets. I'd like the labels to be pretty close vertically so instead of specifying a point size I just use a single - between them, which means "a recommended standard amount of space." This is the same distance you'd get using the snap guides in Interface Builder.

    [constraints addObjectsFromArray:
     [NSLayoutConstraint
          constraintsWithVisualFormat:@"V:|-[_titleLabel]-[_urlLabel]-[_tagsLabel]-|"
                              options:0
                              metrics:nil
                                views:views]];

We wrap up the method by adding our array of constraints to the content view of the cell. We then call [super updateConstraints] to tell Auto Layout we're done.

    [self.contentView addConstraints:constraints];
    [super updateConstraints];
}

Finally we have our little convenience method that populates the labels with text:

-(void)populateWithBookmark:(Bookmark *)bookmark
{
    _titleLabel.text = bookmark.title;
    _urlLabel.text = bookmark.url;
    _tagsLabel.text = bookmark.tags;
}

@end

That's it for our MagicTableViewCell subclass! Before we're done, though, there are a couple things you need to remember to do your MagicTableViewController to make it support your auto-sizing cell:

Aside: UILabels and Rotation

There are some issues with UILabel in the latest SDK that can cause a label to not properly resize when the device is rotated. I went through a bunch of different techniques to fix this (which I'll save for their own post) but in the end the fairly unsatisfactory answer I came up with is to just reload the table view when your device rotates. You can do that in your TableViewController like so:

-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        [self.tableView reloadRowsAtIndexPaths:self.tableView.indexPathsForVisibleRows withRowAnimation:UITableViewRowAnimationNone];
    } completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {

    }];
}

The End

I hope the fruit of my struggles with auto-resizing cells will help save you from your own. If there are any details you think you might still be missing, you can download my complete demo application from Github and tear it apart yourself.

But wait, what about Swift?

Don't even get me started.

Alright, alright. I spent a little time converting the classes in the demo app to Swift. It's a pretty straightforward rewrite of the Objective-C code and I didn't use any particularly tricky Swift features.

Edited: Sept 24, 2014