Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom TableViewCell triggers NSLayoutConstraint error 'UIView-Encapsulated-Layout-Height'

I use Automatic Height for cells. And each time I want to update my cell with new data (model variable here) I updated my autolayout constraints and I get an error. Here just to show the issue, I don't even change the constraints. I simply ask to recalculate the layout.

At the first init of the cell : No warning, no problem.

The error:

<NSLayoutConstraint:0x174285d70 'UIView-Encapsulated-Layout-Height'
 UITableViewCellContentView:0x1030f8a50.height == 500   (active)>

The code:

tableView.rowHeight = UITableViewAutomaticDimension

override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
      return 500
  }

class TestTableViewCell: UITableViewCell {

  var model: X? {
    didSet {
      setNeedsLayout()
      layoutIfNeeded()
    }
  }

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

  override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    let viewtop = UIView()
    let viewbottom = UIView()

    viewtop.backgroundColor = UIColor.yellow
    viewbottom.backgroundColor = UIColor.red
    contentView.backgroundColor = UIColor.blue

    contentView.addSubview(viewtop)
    contentView.addSubview(viewbottom)

    viewtop.snp.makeConstraints { (make) in
      make.top.equalTo(contentView)
      make.left.right.equalTo(contentView)
      make.height.equalTo(50)
    }

    viewbottom.snp.makeConstraints { (make) in
      make.top.equalTo(viewtop.snp.bottom)
      make.left.right.equalTo(contentView)
      make.bottom.equalTo(contentView)
      make.height.equalTo(120)
    }
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

}

The question: Why, after asking a re-layout of the same constraints, do I get an error ?

EDIT: another example for better comprehension here.

var botViewHeightConstraint:Constraint!
class TestTableViewCell: UITableViewCell {

  var model: Int? {
    didSet {
      if model == 1 {
        botViewHeightConstraint.update(offset:200)
      }else{
        botViewHeightConstraint.update(offset:120)
      }
    }
  }

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

  override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    let viewtop = UIView()
    let viewbottom = UIView()

    viewtop.backgroundColor = UIColor.yellow
    viewbottom.backgroundColor = UIColor.red
    contentView.backgroundColor = UIColor.blue

    contentView.addSubview(viewtop)
    contentView.addSubview(viewbottom)

    viewtop.snp.makeConstraints { (make) in
      make.top.equalTo(contentView)
      make.left.right.equalTo(contentView)
      make.height.equalTo(50)
    }

    viewbottom.snp.makeConstraints { (make) in
      make.top.equalTo(viewtop.snp.bottom)
      make.left.right.equalTo(contentView)
      make.bottom.equalTo(contentView)
      botViewHeightConstraint = make.height.equalTo(120).constraint
    }
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

}

CellForRow code:

 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    if let post = fetchedResultsController?.fetchedObjects?[(indexPath as NSIndexPath).section]  {
       let cell = tableView.dequeueReusableCell(withIdentifier: imagePostCellId) as! TestTableViewCell!
       cell.model = 1
       return cell
    }
}
like image 701
Mikael Avatar asked Mar 11 '23 01:03

Mikael


1 Answers

First, you keep neglecting to set your subviews' translatesAutoresizingMaskIntoConstraints to false. That's important. (Perhaps snapkit does that for you, however. I don't know.)

Second — and this is the big point — what you're doing is not how you size a variable height cell from the inside out. You do not change an absolute height constraint, as your code is doing. The sizing is based ultimately on the intrinsic content size of the subviews. Some subviews might have absolute heights, certainly, but ultimately there must be at least one with an intrinsic content size. That is the size that you are able to change dynamically in order to determine the height of a cell.

That is why, for example, a cell containing a UILabel is so easy to use with dynamic row heights. It has an intrinsic content size.

The intrinsic content size does not conflict with the built-in height of the cell (the UIView-Encapsulated-Layout-Height in your console dump); it supplements it (when the runtime calls systemLayoutSizeFitting(UILayoutFittingCompressedSize) behind the scenes, which is how automatic variable row heights works).

If you use custom subviews with an implementation of intrinsicContentSize, and if setting your model value in the cell triggers a call to invalidateIntrinsicContentSize, your example will work perfectly with no complaints in the console.

Here is an example of such a custom view:

class MyView : UIView {
    var h : CGFloat = 200 {
        didSet {
            self.invalidateIntrinsicContentSize()
        }
    }
    override var intrinsicContentSize: CGSize {
        return CGSize(width:300, height:self.h)
    }
}

When this is a subview of your cell's content view, setting this view's h in cellForRow sizes the cell's height correctly.

For example, let's suppose our cell's content view has just one subview, v, which is a MyView. Then:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyCell
    let even = indexPath.row % 2 == 0
    cell.v.backgroundColor = even ? .red : .green
    cell.v.h = even ? 40 : 80 // triggers layout!
    return cell
}
like image 59
matt Avatar answered May 07 '23 02:05

matt