Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

Current status

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).

Screenshot initial

  • 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.

Screenshot detailed

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?

like image 717
J贸zsef Vesza Avatar asked May 06 '15 13:05

J贸zsef Vesza


People also ask

How do you make a table dynamic cell height?

To change the height of tableView cell in ios dynamically, i.e resizing the cell according to the content available, we'll need to make use of automatic dimension property. We'll see this with the help of an sample project.

What is Estimatedrowheight?

The estimated height of rows in the table view.

What is Uitableview in Swift?

A view that presents data using rows in a single column. iOS 2.0+ iPadOS 2.0+ Mac Catalyst 13.1+ tvOS 9.0+


1 Answers

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:

Main View

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.

Detail View

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:

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.

like image 198
J贸zsef Vesza Avatar answered Oct 19 '22 16:10

J贸zsef Vesza