Question

I'm learning / experimenting with autolayout and UITableViewCell's. I asked another question a few days ago to which I answered my own question, I'm still using the same constraints / code. For the full code see here: AutoLayout multiline UILabel cutting off some text .

To cut it short inside heightForRowAtIndexPath I am using an instance of a custom UITableViewCell to calculate the height the row needs to be. This works perfect in portrait, however when I switch to landscape mode, systemLayoutSizeFittingSize is returning the same height for the cell as if it was in portrait. I've printed out the frames of the contentView and the labels and nothing seems to be updating.

The result of this is the constraints are forcing the labels to grow leaving a huge amount of whitespace. The labels display in the correct width, in landscape they are laid out as I would expect, If I hardcode the height of the cell it works perfectly.

It looks like this: enter image description here

After hardcoding (what I want it to look like): enter image description here

Even worse I get the same result when running on iPad, even portrait mode, meaning I get the iPhone dimensions. From what I'm seeing it is as though systemLayoutSizeFittingSize has no concept of orientation or device for that matter.

I've tried faking the frame the cell should be, tried rotating the cell, calling layoutSubviews, reloading the tableView on orientation change and nothing seems to affect it.

Have I missed something basic ?

Was it helpful?

Solution

@rdelmar has the right approach. You definitely need to reset the preferredMaxLayoutWidth on each label before you call systemLayoutSizeFittingSize on the contentView. I also use a UILabel subclass with the layoutSubviews method as he demonstrates.

The main downside to an autolayout approach like this is the overhead. We're effectively running autolayout three times for each cell that will be displayed: Once to prepare the sizing cell for systemLayoutSizeFittingSize (size it and set preferredMaxLayoutWidth on each sublabel), again with the call to systemLayoutSizeFittingSize, and again on the actual cell we return from cellForRowAtIndexPath.

Why do we need/want the first autolayout pass? Without it we don't know what width to set our child labels preferredMaxLayoutWidth values to. We could hardcode the values as in @rdelmars example (which is fine) but it's more fragile if you change your cell layout or have lots of cell types to deal with.

If the main issue is recalculating on orientation change then the code below can likely be optimized to run the first layout pass just once per orientation change.

Here's the pattern I use, which negates the need to manipulate the cell controls in the view controller. It's more encapsulated but arguably more expensive.

- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // assumes all cells are of the same type!
    static UITableViewCell* cell;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        cell = [tableView dequeueReusableCellWithIdentifier: @"label_cell"];
    });

    // size the cell for the current orientation.  assume's we're full screen width:
    cell.frame = CGRectMake(0, 0, tableView.bounds.size.width, cell.frame.size.height );

    // perform a cell layout - this runs autolayout and also updates any preferredMaxLayoutWidths via layoutSubviews in our subclassed UILabels
    [cell layoutIfNeeded];

    // finally calculate the required height:
    CGSize s = [cell.contentView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize];

    return s.height + 1; // +1 because the contentView is 1pt shorter than the cell itself when there's a separator.  If no separator you shouldn't need +1
}

along with:

@interface TSLabel : UILabel
@end

@implementation TSLabel

- (void)layoutSubviews
{
    self.preferredMaxLayoutWidth = CGRectGetWidth(self.bounds);
    [super layoutSubviews];
}

@end

OTHER TIPS

I'm still trying to figure out how to make systemLayoutSizeFittingSize work correctly, especially when I have multiple multi-line labels. I have one test app that seems to work, but why it works, is still a bit of a mystery. So, I have a subclassed label that has the same code that you're using,

- (void)layoutSubviews {
    self.preferredMaxLayoutWidth = CGRectGetWidth(self.bounds);
    [super layoutSubviews];
}

In heightForRowAtIndexPath;, I'm also setting the preferredMaxWidth (using some hard coded numbers based on the constraints). If I remove either of these calls to preferredMaxWidth, it doesn't work properly. Removing the ones in heightForRowAtIndexPath gives me extra space in my labels, similar to what you're seeing.

- (CGFloat) tableView: (UITableView *) tableView heightForRowAtIndexPath: (NSIndexPath *) indexPath {
    static RDCell *sizingCell;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sizingCell = (RDCell*)[tableView dequeueReusableCellWithIdentifier: @"Cell"];
    });
    sizingCell.label.text = self.theData[indexPath.row];
    sizingCell.bottomLabel.text = self.theData2[indexPath.row];
    sizingCell.label.preferredMaxLayoutWidth = tableView.bounds.size.width - 40; // the 40 comes from the 20 point spacing constraint to each side of the cell setup in the storyboard
    sizingCell.bottomLabel.preferredMaxLayoutWidth = tableView.bounds.size.width - 40;
    return [sizingCell.contentView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize].height + 2; // the calculation seems to be low by 1 (per label), so add 2 for my 2 labels
}

In this test project, my custom cell has two labels arranged over top of each other. I know this isn't an ideal solution, I'm still testing to see why this works, and why it doesn't with just one of the uses of preferredMaxWidth. In preliminary tests, it looks like it might be a timing issue, as to when the various methods are called at startup and when they're called on rotation.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top