3
votes

I created a tableview with self-sizing UITableViewCells using

tableview.rowHeight = UITableViewAutomaticDimension;

For testing a created a single UIView and added it to cell.contentView with the following constraints. (using masonry)

- (void)updateConstraints {
    UIView *superview = self.contentView;

    if (!_didSetupConstraints) {
        [self.testView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(superview);
            make.height.equalTo(@10.0f);
            make.left.equalTo(superview);
            make.right.equalTo(superview);
            make.bottom.equalTo(superview);
        }];

        _didSetupConstraints = YES;
    }

    [super updateConstraints];
}

When i select a row i like to change the height of the selected cell. I do this by:

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

    [self.testView mas_updateConstraints:^(MASConstraintMaker *make) {
        if (selected) {
            make.height.equalTo(@100);
        }
        else {
            make.height.equalTo(@10);
        }
    }];

    [UIView animateWithDuration:0.3f animations:^{
        [super layoutIfNeeded];
    }];
}

It seems to work but, xcode complaints with the following error:

Unable to simultaneously satisfy constraints.

Then problem is that contentView is creating a height constaint on its own (because contentView.translatesAutoresizingMaskIntoConstraints = YES; per default).

So when updateConstraints is first run, the system will create a height constraint on contentView equal to 10.0f

afterwards when setSelected... is called i alter the height of my testView so theres a conflict between testView.height (100.0f) and contentView.height (10.0f) and since testView is attached to the bottom of contentView (in order for self-sizing cells to work) it gives and error.

I tried setting contentView.translatesAutoresizingMaskIntoConstraints = NO; but then it seems the UITableViewCell cant really figure out a proper size for the cell. (sometimes it too wide / too small) etc.

What is the proper way to implement self-sizing cells which can have their height changed dynamicly?

1
Did you set the estimatedRowHeight property? - Michael
I did not set estimatedRowHeight :) but just tested and it does not have any effect on the issue with conflicting height constraints. - Mads Lee Jensen

1 Answers

0
votes

There are two issues here:

1. The AutoLayout message: Unable to simultaneously satisfy constraints.

This is caused by the fact that you update your custom view's height constraint, but you do not update the contentView's height constraint. Because you also set the custom view's bottom constraint to the contentView bottom, the system cannot satisfy both contstraints: The custom view cannot have a height of 100 and at the same time be connected to the contentView's bottom while the contentView still has a height of 10.

The fix for this is quite easy. You define a height constraint for the contentView and set it to the height of your custom view:

[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.height.equalTo(self.testView);
}];

This fixes the problem.

2. Updating the cell's height

Updating the cell height is not so easy because the cell heights are handled by the UITableViewController and not the cells themselves. Of course you can change the height of your cell, but as long as the UITableViewController does not give the cell enough space you won't see the new cell height. You will see that your now higher cell will overlap the next cell.

So you need a way to tell the UITableViewController that it should update the height of your cell. You can do that by calling reloadRowsAtIndexPaths:withRowAnimation:. That reloads the cell and also animates it nicely. So the cell somehow has to call that method on the UITableViewController. The challenge there: A UITableViewCell has no reference to its UITableViewController (and it shouldn't).

What you can do is subclass UITableViewCell and give it a closure property that it can execute whenever its height has changed:

class DynamicHeightTableViewCell: UITableViewCell {
    var heightDidChange: (() -> Void)?
    ...

Now, when you dequeue a cell in your UITableViewControllerDataSource you set its closure:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("DynamicHeightTableViewCell", forIndexPath: indexPath)
    if let customCell = cell as? DynamicHeightTableViewCell {
        customCell.heightDidChange = { [weak cell, weak tableView] in
            if let currentIndexPath = tableView?.indexPathForCell(cell!) {
                tableView?.reloadRowsAtIndexPaths([currentIndexPath], withRowAnimation: .Automatic)
            }
        }
    }

    return cell
}

And then when the cell changes its height you just execute the closure and the cell will be reloaded:

func theFunctionWhereTheHeightChanges() {

   // change the height

   heightDidChange?()
}