Marcin Kapusta
Marcin Kapusta

Reputation: 5396

UITableViewCell height dynamically calculated with autolayout based on row content

I've prepared UITableViewCell in nib file with all constraints from top to bottom. Calculating row height in table view seems to work.

But if I want to activate/deactivate some other constraints in the cell based on some row data condition then I have problem because it seems like the height is not calculated then. What is the problem? In the following example TableRowDM.hidden means should subtitle be hidden or not. I'm hidding this UILabel by activating not activated constraint (height with constant = 0) and setting vertical gap between titleLabel and subtitleLabel to 0.

struct TableRowDM {
    let title: String
    let subTitle: String
    let hidden: Bool
}

let cellId: String = "CellView"

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    @IBOutlet weak var tableView: UITableView!

    let rowData: [TableRowDM] = [
        TableRowDM(title: "Row 1", subTitle: "Short title 1th row", hidden: false),
        TableRowDM(title: "Row 2", subTitle: "Short title 2th row", hidden: true),
        TableRowDM(title: "Row 3", subTitle: "Very long text in subtitle at 3th row to test text wrapping and growing subtitle height", hidden: false),
        TableRowDM(title: "Row 4", subTitle: "Long text in subtitle at 4th row", hidden: false),
        TableRowDM(title: "Row 5", subTitle: "Long text in subtitle at 5th row", hidden: true),
        TableRowDM(title: "Row 6", subTitle: "Long text in subtitle at 6th row", hidden: false),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.estimatedRowHeight = 50
        tableView.tableFooterView = UIView(frame: CGRect.zero)
        tableView.register(UINib(nibName: cellId, bundle: nil), forCellReuseIdentifier: cellId)
        tableView.delegate = self
        tableView.dataSource = self
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! CellView
        let row: TableRowDM = self.rowData[indexPath.row]

        cell.title.text = row.title
        cell.subtitle.text = row.subTitle

        if row.hidden {
            // hide subtitle
            cell.subtitleHeight.isActive = true
            cell.titleToSubtitleGap.constant = 0
        } else {
            // show subtitle
            cell.subtitleHeight.isActive = false
            cell.titleToSubtitleGap.constant = 16
        }
        cell.setNeedsLayout()
        cell.layoutIfNeeded()
        return cell
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return rowData.count
    }
}

It seems that this is not working and subtitleLabel is not hiding and row height is always the same. When contents of subtitleLabel and titleLabel changes then the height is adjusted to the text inside those UIViews but when I try to manipulate with constraints then it is not working. Any help? What Am I doing wrong? The CellView is very simple view with such subviews

-----------------
      |
- titleLabel ----
      |
      | <- titleToSubtitleGap
      |
- subtitleLabel -
      |
-----------------

Upvotes: 0

Views: 38

Answers (1)

Marcin Kapusta
Marcin Kapusta

Reputation: 5396

I found a solution for my question. It is not documented anywhere but it works.

What is important. If You are supporting landscape/portrait orientations then it is important to set right priorities in NSLayoutConstraints that defines vertical unbroken chain of constraints and views (with defined heights) as stated here - they wrote:

Next, lay out the table view cell’s content within the cell’s content view. To define the cell’s height, you need an unbroken chain of constraints and views (with defined heights) to fill the area between the content view’s top edge and its bottom edge. If your views have intrinsic content heights, the system uses those values. If not, you must add the appropriate height constraints, either to the views or to the content view itself.

What they didn't wrote is that all those vertical constraints that define finally the height of the table cell contentView must have priorities less than required (1000) if You need to support many device orientations. I set in my examples those priorities to High (750).

Next it is important to make changes with those constraints in updateConstraints method. So if You are designing a UIView and when some property of this view need to change constraints then You need to do it something like this in Your view so updateConstraints method will be called in right moment. After setting the property new value call needsUpdateConstraints() so AutoLayout will know that constraints in our UIView changed.

import UIKit

class ListItemTableViewCell: UITableViewCell {

    @IBOutlet weak var subtitleToTitleGap: NSLayoutConstraint!
    @IBOutlet weak var subtitleHeightZero: NSLayoutConstraint!

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!

    public var isSubtitleHidden: Bool {
        get {
            return self.subtitleLabel.isHidden
        }
        set {
            // Here we are changing property that change layout in our view
            self.subtitleLabel.isHidden = newValue
            self.needsUpdateConstraints() // Here we are telling AutoLayout to recalculate constraints in `updateConstraints` method.
        }
    }

#if !TARGET_INTERFACE_BUILDER
    // We need to block this code from running in Interface Builder
    override func updateConstraints() {
        if self.isSubtitleHidden {
            // Hide Subtitle
            self.subtitleToTitleGap.constant = 0.0
            self.subtitleHeightZero.autoInstall() // This makes subtitle height to be 0
        } else {
            // Show Subtitle
            self.subtitleToTitleGap.constant = 2.0
            self.subtitleHeightZero.autoRemove() // This will make subtitle height to be self calculated
        }
        super.updateConstraints() // don't forget to call this after You finished constraints changes
    }
#endif
}

And the most important and it was the most difficult for me to find out is to hide implementation of updateConstraints from interface builder because it was causing IB to crash from time to time in xib editor. Look above for this example. The method is wrapped in #if preprocessor macro. So I think thats it. Good Luck!

Upvotes: 0

Related Questions