AutoSize UITableViewCell height programmatically

We've been working with NSLayoutConstraint and the AutoSize system to dynamically set sizes. There are not many tutorials about using this system programmatically... and definitely not much explanation with autosizing cells.

So... I spent a few hours today and figured out the bare minimum you need to make UITableViewCell's dynamically set their height based on a UILabel's content.

If you search online, you'll likely encounter this Stackoverflow article that discusses the topic in detail. The corresponding code example for iOS7 is extremely heavy weight. I took what was done there, reverse engineered it, and pared it back to the core library. You can get this functionality with a few lines of code. Yay!

UITableViewCell

First lets start with a custom UITableViewCell.

@implementation AutoSizeCell2

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

        self.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
        self.textLabel.numberOfLines = 0;
        self.textLabel.translatesAutoresizingMaskIntoConstraints = NO;

        [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-6-[bodyLabel]-6-|" options:0 metrics:nil views:@{ @"bodyLabel": self.textLabel }]];
        [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-6-[bodyLabel]-6-|" options:0 metrics:nil views:@{ @"bodyLabel": self.textLabel }]];
    }

    return self;
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    // Make sure the contentView does a layout pass here so that its subviews have their frames set, which we
    // need to use to set the preferredMaxLayoutWidth below.
    [self.contentView setNeedsLayout];
    [self.contentView layoutIfNeeded];

    // Set the preferredMaxLayoutWidth of the mutli-line bodyLabel based on the evaluated width of the label's frame,
    // as this will allow the text to wrap correctly, and as a result allow the label to take on the correct height.
    self.textLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.textLabel.frame);
}

@end

For starters, in the initWithStyle method we set up the textLabel so that it wraps correctly by setting a few properties.

self.textLabel.lineBreakMode = NSLineBreakByWordWrapping;  
self.textLabel.numberOfLines = 0;  

lineBreakMode will specify one of the NSLineBreakMode values for how line break should be handled. Setting numberOfLines to 0 removes limits for the number of lines that can be used. This now means our UILabel will wrap and contintue to wrap until there is no more wrapping to do.

The next part is all about layout constraints.

self.textLabel.translatesAutoresizingMaskIntoConstraints = NO;

[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-6-[bodyLabel]-6-|" options:0 metrics:nil views:@{ @"bodyLabel": self.textLabel }]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-6-[bodyLabel]-6-|" options:0 metrics:nil views:@{ @"bodyLabel": self.textLabel }]];

The translatesAutoresizingMaskIntoConstraints is a property inherited from UIView. Basically, it enables AutoSizing via the constraint system.

The next two lines are the actual constraints. If you're unfamiliar with the syntax you can refer to the Auto Layout Guide for a full workup on how you can programmatically create constraints.

In this example, it says add 6 points of margin between the edges in both the horizontal (first constraint) and vertical (second constraint).

Notice that the constraints are added to the contentView property and not directly on self. This is extremely important or you will spend a few hours banging your head against the wall and cursing Apple.

Additionally, any other views that you add to the cell need to be added to the contentView for auto-sizing to work with them.

The final part is the layoutSubviews method. This method is responsible for setting the preferredMaxLayoutWidth on the label. Setting this is required and allows the text to wrap correctly and take on the correct height. Again, note that the setNeedsLayout and layoutIfNeeded methods are called on the contentView of the cell.

UITableViewController

After you have a UITableViewCell ready to go, you can modify your controller. The below example is contrived to show a single row that will overflow a single line.

@implementation AutoSizingController

#pragma mark - Table view data source

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

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

- (NSString *)getText
{
    return @"This is some long text that should wrap. It is multiple long sentences that may or may not have spelling and grammatical errors. Yep it should wrap quite nicely and serve as a nice example!";
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Create a reusable cell
    AutoSizeCell2 *cell = [tableView dequeueReusableCellWithIdentifier:@"plerp"];
    if(!cell) {
        cell = [[AutoSizeCell2 alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"plerp"];
    }

    // Configure the cell for this indexPath
    cell.textLabel.text = [self getText];

    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    AutoSizeCell2 *cell = [[AutoSizeCell2 alloc] init];
    cell.textLabel.text = [self getText];

    // Do the layout pass on the cell, which will calculate the frames for all the views based on the constraints
    // (Note that the preferredMaxLayoutWidth is set on multi-line UILabels inside the -[layoutSubviews] method
    // in the UITableViewCell subclass
    [cell setNeedsLayout];
    [cell layoutIfNeeded];

    // Get the actual height required for the cell
    CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    // Add an extra point to the height to account for the cell separator, which is added between the bottom
    // of the cell's contentView and the bottom of the table view cell.
    height += 1;

    return height;
}

@end

The implementation is your standard UITableView implementation. The only area that is we had to change was the to implement the heightForRowAtIndexPath method.

In this method, we construct a cell as if we were going to use it. Note that you should be applying hte values to the cell as if you were using it.

We then call layoutIfNeeded.

Finally, we call systemLayoutSizeFittingSize on the cell's contentView with the UILayoutFittingCompressedSize value to retrieve the actual content size of the cell.

That's all there is to it!

You can download a full working example on github: https://github.com/bmancini55/iOSExamples-AutoSizingTableCells

comments powered by Disqus