Diego Cuadros
Diego Cuadros

Reputation: 316

UITableViewCell contentView has autoResizingMask constraint 'UIView-Encapsulated-Layout-Height' clashing with autolayout for self resizing

I'm trying to create a table view whose cells expand when the cell is selected and remain expanded until selected again.

I'm trying to get the height of the cells to be determined by their content. I already know of setting a tableView's rowHeight and estimagedRowHeight to get self sizing working, so that's not the issue.

ViewController.swift:

import UIKit

class ViewController: UIViewController {

    lazy var tableView: UITableView = {
        let tv = UITableView(frame: .zero, style: .plain)
        tv.register(ExpandingCell.self, forCellReuseIdentifier: ExpandingCell.reuseIdentifier)
        tv.estimatedRowHeight = 85.0
        tv.rowHeight = UITableViewAutomaticDimension
        return tv
    }()


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        self.view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
            tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)])


        tableView.dataSource = self
        tableView.delegate = self
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 50
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: ExpandingCell.reuseIdentifier, for: indexPath) as! ExpandingCell
        return cell
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableViewAutomaticDimension
    }
    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        return 50.0
    }

}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    }
    func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {

    }
}

ExpandingCell.swift

import UIKit

class ExpandingCell: UITableViewCell {
    static let reuseIdentifier = "dafasdfadfagr"

    let minimumDisplayedView = UIView()
    let extraContentView = UIView()
    var expandingConstraint: NSLayoutConstraint?

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    private func commonInit() {
        self.setupViews()
        self.contentView.clipsToBounds = true
    }
    private func setupViews() {
        self.addSubviews()
        self.customizeSubviews()
        self.constrainSubviews()
    }

    private func addSubviews() {
        self.contentView.addSubview(minimumDisplayedView)
        self.contentView.addSubview(extraContentView)
    }
    private func customizeSubviews() {
        self.customizeMinimumDisplayedView()
        self.customizeExtraContentView()
    }
    private func constrainSubviews() {
        self.constrainMinimumDisplayedView()
        self.constrainExtraContentView()
    }

    // MARK: - MinimumDisplayView
    private func customizeMinimumDisplayedView() {
        minimumDisplayedView.backgroundColor = .blue
    }
    private func constrainMinimumDisplayedView() {
        minimumDisplayedView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            minimumDisplayedView.heightAnchor.constraint(equalToConstant: 50),
            minimumDisplayedView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
            minimumDisplayedView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
            minimumDisplayedView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor)
            ])
        expandingConstraint = minimumDisplayedView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor)
        expandingConstraint?.isActive = true
    }

    // MARK: - ExtraContentView
    private func customizeExtraContentView() {
        extraContentView.backgroundColor = .red
    }
    private func constrainExtraContentView() {
        extraContentView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            extraContentView.heightAnchor.constraint(equalToConstant: 200),
            extraContentView.topAnchor.constraint(equalTo: minimumDisplayedView.bottomAnchor),
            extraContentView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
            extraContentView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor)
            ])
    }

    // MARK: - Animate Expansion
//    override func setSelected(_ selected: Bool, animated: Bool) {
//        super.setSelected(selected, animated: animated)
//        print("selected: \(selected)")
//        resize(selected)
//    }

    private func resize(_ expand: Bool) {
        expandingConstraint?.isActive = false
        if expand {
            expandingConstraint = extraContentView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor)
            expandingConstraint?.isActive = true
        } else {
            expandingConstraint = minimumDisplayedView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor)
            expandingConstraint?.isActive = true
        }
        UIView.animate(withDuration: 0.3) {
            self.layoutIfNeeded()
        }
    }

}

I have 2 views being added to the contentView and pinning the edges of the contentView to only the first view when initializing the cell. The first view is blue and short, while the second is red and tall.

I would expect the tableView to look all blue initially, but it instead looks like this: incorrectly shown cells

I suspect it has something to do with the way cells are laid out in a view lifecycle, so I also came across this answer: reload tableview after first load as a workaround

When I run the code I get these constraint errors

2018-05-05 18:00:15.108634-0400 TestExpandingCells[1022:13932317] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. 
Try this: 
    (1) look at each constraint and try to figure out which you don't expect; 
    (2) find the code that added the unwanted constraint or constraints and fix it. 
(
"<NSLayoutConstraint:0x6000000961c0 UIView:0x7fe583700df0.height == 50   (active)>",
"<NSLayoutConstraint:0x600000096580 V:|-(0)-[UIView:0x7fe583700df0]   (active, names: '|':UITableViewCellContentView:0x7fe583704850 )>",
"<NSLayoutConstraint:0x600000096760 UIView:0x7fe583700df0.bottom == UITableViewCellContentView:0x7fe583704850.bottom   (active)>",
"<NSLayoutConstraint:0x600000096bc0 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7fe583704850.height == 50   (active)>"
)

The last constraint on that list, 'UIView-Encapsulated-Layout-Height', seems to come from some autoresizingmask-to-constraint thing UIKit creates for laying out cell.

So my question is, how can I remove this constraint to allow AutoLayout to completely handle the making of my cells, without a workaround. I would also prefer to have it all contained in the ExpansionCell class, rather than have it in the tableview's delegate methods.

Bonus points if you also find a way to trigger the expansion of the cell using AutoLayout.

Upvotes: 2

Views: 3461

Answers (2)

SimranChahal
SimranChahal

Reputation: 5

You can do 2 things:

  1. Lower the priority of any of vertical constraints to less than required.

    verticalAnchor.priority = .defaultHigh
    
  2. tableView should have rowHeight and estimatedRowHeight:

     tableView.rowHeight = UITableView.automaticDimension
     tableView.estimatedRowHeight = xx.xx (any value)
    

Upvotes: -1

Shehata Gamal
Shehata Gamal

Reputation: 100523

First to get rid of the constraints's conflict set the priority of the bottom constraint of any view connected to contentView to 999 , second to trigger expanding hook the height constraint and change it , Also in your model declare any array of bool values to determine whether a cell is expanded or not to restore cell state after scroll and

in cellForRow

cell.viewHeighcon.constant = isExpanded ? 200 : 50

Upvotes: 3

Related Questions