8

I have a UITableViewCell subclass which contains a multiline label, and I would like the cell to size itself dynamically based on the content of that label. I'm aware that iOS 8 introduced auto-sizing cells based on AutoLayout constraints, and I've found several examples of this already on SO, but I'm still having some trouble implementing this behavior properly.

Here's my updateConstraints implementation:

- (void)updateConstraints {
    [super updateConstraints];

    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-10-[_nameLabel(==20)]-10-[_tweetLabel]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_nameLabel, _tweetLabel)]];
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_avatarView]-10-[_nameLabel]-10-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_avatarView, _nameLabel)]];

    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_nameLabel]-10-[_tweetLabel]-10-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_nameLabel, _tweetLabel)]];
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_avatarView]-10-[_tweetLabel]-10-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_avatarView, _tweetLabel)]];

    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-10-[_avatarView(==45)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_avatarView)]];
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[_avatarView(==45)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_avatarView)]];
}

In the table view controller I set the row height to UITableViewAutomaticDimension (and I set an estimated row height as well). At runtime, I get a series of auto layout errors and all of the table view cells appear nearly completely overlapped.

The auto layout conflicts are between the following constraints:

  • V:|-(10)-[_nameLabel]
  • V:[_nameLabel(20)]
  • V:[_nameLabel]-(10)-[_tweetLabel]
  • V:[_tweetLabel]-(10)-|
  • V:[cell(44)]

I suspect the last constraint, "UIView-Encapsulated-Layout-Height", which forces a height of 44, is the cause of the issue, but I'm not quite sure where that comes from, so hopefully somebody can shed some light on the issue.

futurevilla216
  • 982
  • 3
  • 13
  • 29
  • 2
    Did you check to see if the translatesAutorezisingMaskIntoConstraints was set to NO on your UITableViewCell? – kljhanson Aug 01 '14 at 20:18
  • @kljhanson Just tried that, thanks for the suggestion, but now a large gray rectangle takes up most of the screen (Xcode's view inspecting says it's hundreds of cell separators). I am definitely giving the right cell count in the delegate method, any idea why that could be happening? – futurevilla216 Aug 01 '14 at 20:50
  • Not sure if this is an Apple autolayout bug or not but users in this post here suggests it is: http://stackoverflow.com/questions/25059443/what-is-nslayoutconstraint-uiview-encapsulated-layout-height-and-how-should-i I'm also getting the same (44) cell height for my own dynamic tableviewcell heights constraint conflicts. It only seems to show up in iOS 8, iOS 7 is working. Strangely enough, it doesn't have this problem if all your cells are the same height and you're not doing dynamic cell height. I really hope it is an Apple bug >.< (note: I'm using pre-iOS 8 dynamic cell height codes) – Zhang Sep 20 '14 at 08:18

3 Answers3

10

In order to implement automatic row heights for table view cells, you need to do the following:

  1. Implement Auto Layout constraints within the cell's contentView that allow the view to express its preferred height. Be sure to set UILabels to word wrap over multiple lines.

    Be sure you've defined an axial chain of constraints in both dimensions, that is, constraints that collectively bind all the way from one edge of the view to the other. Perhaps the easiest way to be sure these constraints are correct is to implement your custom content as a plain old UIView (which is easy to test), and then use constraints so that the UITableViewCell.contentView hugs that view. (I use this gist to automate building the "view-wrapping cell".)

  2. Set tableView.rowHeight = UITableViewAutomaticDimension

  3. Set tableView.estimatedRowHeight = 400 or some other reasonably generous value, in order to workaround some UIKit bugs when the estimate is too low.

I have spent a puzzling amount of time working with this feature. This github repo shows seven complete examples of self-sizing table view cells containing a single label of wrapping text -- programmatic, nib-based, storyboard-based, etc..

Finally, do not worry too much if you see warnings about unsatisfiable constraints mentioning "UIView-Encapsulated-Layout-Height" or similar at the first time the table view loads. This is an artefact of UITableView's initial process for creating a cell, determining what its size should be based on Auto Layout Constraints, and keeping the UITableViewCell tightly wrapping its contentView. The repo I mentioned above has more extensive discussion and code for exploring this somewhat awkward corner of the API.

You should only worry about constraint-violation warnings if they persist even after the cell has loaded and has scrolled a bit, or if you are seeing incorrect layouts initially. In this case, again, the first step should always be to ensure your constraints are correct by developing them and testing them in isolation if possible, in a plain UIView.

Community
  • 1
  • 1
algal
  • 27,584
  • 13
  • 78
  • 80
4

I just came across this issue.

From numerous other Stackoverflow post they recommend:

self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight;

That didn't work for me at first. I found that I also need to do:

self.frame = CGRectMake(0, 0, self.frame.size.width, 50);

My custom cell's init method looks like his:

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

    if(self)
    {
        [self initViews];
        [self initConstraints];
    }

    return self;
}

I put the code in my "initViews" method:

-(void)initViews
{
    ...

    // fixes an iOS 8 issue with UIViewEncapsulated height 44 bug
    self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
    self.frame = CGRectMake(0, 0, self.frame.size.width, 50);
}

The problem went away and my cell looks correct too.

Does this work for you?

Zhang
  • 11,549
  • 7
  • 57
  • 87
  • Thanks! In my case the views looked OK but somehow I got a lot of warnings. I added the mask and frame like you suggested. I guess the problem is that the default height of 44 was to small for my views to fit, and even though the height stretches on display, the internal layout manager complains anyway. I needed a minimum of 200, so I set a secure frame of (0,0,w,200). – Ferran Maylinch Sep 26 '15 at 19:21
2

Are you sure you have -translatesAutoresizingMaskIntoConstraints set to NO on the cell? If you don't, the system generates constraints based on the autoresizing mask, which was the previous way of doing layout on iOS, and should be disabled when using Auto Layout.

wander
  • 711
  • 7
  • 23
  • Thanks for pointing that out, I did set that for the subviews of the cell but not for the cell itself. – futurevilla216 Aug 01 '14 at 20:32
  • But an interesting thing happened when I did set it on the cell - a gray rectangle now takes up most of the screen, and based on Xcode's view inspection it's hundreds upon hundreds of table view separators, any idea what could be causing that? I am returning the correct row count in the delegate callback, so not sure what could be the cause. – futurevilla216 Aug 01 '14 at 20:33
  • No idea if this helps at all, but Apple's documentation for 'updateConstraints' explicitly states that '[super updateConstraints]' should be called as the final step in your implementation. – wander Aug 02 '14 at 01:04