简体   繁体   中英

Dynamically size Table View Cells using Auto Layout constraints

Update

I have revised the question completely after my latest findings.

Goal

My goal is to implement the following effect:

  1. There is a simple table view
  2. The user selects a row
  3. The selected row expands, revealing another label below the original one

Please note that I am aware, that this can be achieved by inserting/deleting cells below the selected one, I already have a successful implementation using that method .

This time, I want to try to achieve this using Auto Layout constraints.

Current status

I have a sample project available for anyone to check , and also opened an issue . To summarize, here's what I've tried so far:

I have the following views as actors here:

  • The cell's content view
  • A top view, containing the main label ("main view")
  • A bottom view, below the main view, containing the initially hidden label ("detail view")

I have set up Auto Layout constraints within my cell the following way (please note that this is strictly pseudo-language):

  • mainView.top = contentView.top
  • mainView.leading = contentView.leading
  • mainView.trailing = contentView.trailing
  • mainView.bottom = detailView.top
  • detailView.leading = contentView.leading
  • detailView.trailing = contentView.trailing
  • detailView.bottom = contentView.bottom
  • detailView.height = 0

I have a custom UITableViewCell subclass, with multiple outlets, but the most important here is an outlet for the height constraint mentioned previously: the idea here is to set its constant to 0 by default, but when the cell is selected, set it to 44, so it becomes visible:

override func setSelected(selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)
    detailViewHeightConstraint.constant = selected ? detailViewDefaultHeight : 0
    UIView.animateWithDuration(0.3) {
        self.layoutIfNeeded()
    }
}

I have the following result:

当前状态

So the effect is working, but not exactly how I originally imagined. Instead of pushing the main view up, I want the cell's height to grow when the detail view is shown, and shrink back when it's hidden.

I have examined my layout hierarchy during runtime:

  • The initial state is OK. The height of the content view is equal to the height of my main view (in this case, it's 125 points).

截图初始

  • When the cell is selected, the height constraint of the detail view is increased to 44 points and the two views are properly stacked vertically.But instead of the cell's content view extending, but instead, the main view shrinks.

截图详细

Question

What I need is the following: the height of table view cell's content view should be equal to

  • the height of the main view, when the detail view's height constraint is 0 (currently this works)
  • main view height + detail view height when the detail view's height constraint is set properly (this does not work).

How do I have to set my constraints to achieve that?

After a significant amount of research, I think I've found the solution with the help of this great article.

Here are the steps needed to make the cell resize:

Within the Main, and Detail Views, I have originally set the labels to be horizontally and vertically centered. This isn't enough for self sizing cells. The first thing I needed is to set up my layout using vertical spacing constraints instead of simple alignment:

主视图

Additionally you should set the Main Container's vertical compression resistance to 1000.

The detail view is a bit more tricky: Apart from creating the appropriate vertical constraints, you also have to play with their priorities to reach the desired effect:

  • The Detail Container's Height is constrained to be 44 points, but to make it optional, set its priority to 999 (according to the docs, anything lower than "Required", will be regarded such).
  • Within the Detail Container, set up the vertical spacing constraints, and give them a priority of 998.

详细视图

The main idea is the following:

  • By default, the cell is collapsed. To achieve this, we must programmatically set the constant of the Detail Container's height constraint to 0. Since its priority is higher than the vertical constraints within the cell's content view, the latter will be ignored, so the Detail Container will be hidden.
  • When we select the cell, we want it to expand. This means, that the vertical constraints must take control: we set the priority Detail Container's height constraint to something low (I used 250), so it will be ignored in favor of the constraints within the content view.

I had to modify my UITableViewCell subclass to support these operations:

// `showDetails` is exposed to control, whether the cell should be expanded
var showsDetails = false {
    didSet {
        detailViewHeightConstraint.priority = showsDetails ? lowLayoutPriority : highLayoutPriority
    }
}

override func awakeFromNib() {
    super.awakeFromNib()
    detailViewHeightConstraint.constant = 0
}

To trigger the behavior, we must override tableView(_:didSelectRowAtIndexPath:) :

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    tableView.deselectRowAtIndexPath(indexPath, animated: false)

    switch expandedIndexPath {
    case .Some(_) where expandedIndexPath == indexPath:
        expandedIndexPath = nil
    case .Some(let expandedIndex) where expandedIndex != indexPath:
        expandedIndexPath = nil
        self.tableView(tableView, didSelectRowAtIndexPath: indexPath)
    default:
        expandedIndexPath = indexPath
    }
}

Notice that I've introduced expandedIndexPath to keep track of our currently expanded index:

var expandedIndexPath: NSIndexPath? {
    didSet {
        switch expandedIndexPath {
        case .Some(let index):
            tableView.reloadRowsAtIndexPaths([index], withRowAnimation: UITableViewRowAnimation.Automatic)
        case .None:
            tableView.reloadRowsAtIndexPaths([oldValue!], withRowAnimation: UITableViewRowAnimation.Automatic)
        }
    }
}

Setting the property will result in the table view reloading the appropriate indexes, giving us a perfect opportunity to tell the cell, if it should expand:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! ExpandableTableViewCell

    cell.mainTitle = viewModel.mainTitleForRow(indexPath.row)
    cell.detailTitle = viewModel.detailTitleForRow(indexPath.row)

    switch expandedIndexPath {
    case .Some(let expandedIndexPath) where expandedIndexPath == indexPath:
        cell.showsDetails = true
    default:
        cell.showsDetails = false
    }

    return cell
}

The last step is to enable self-sizing in viewDidLoad() :

override func viewDidLoad() {
    super.viewDidLoad()
    tableView.contentInset.top = statusbarHeight
    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.estimatedRowHeight = 125
}

Here is the result:

结果

Cells now correctly size themselves. You may notice that the animation is still a bit weird, but fixing that does not fall into the scope of this question.

Conclusion: this was way harder than it should be. 😀 I really hope to see some improvements in the future.

This is in obj-c, but I'm sure you'll handle that:

Add in your viewDidLoad:

self.tableView.estimatedRowHeight = self.tableView.rowHeight; self.tableView.rowHeight = UITableViewAutomaticDimension;

This will enable self sizing cells for your tableView, and should work on iOS8+

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM