Search code examples
iosswiftuitableviewnslayoutconstraint

Swift 4.2 TableViewCell dynamic height programmatically


This is not duplicated question, because there is not real solution fo this issue

I am trying implement UITableViewcell dynamic height by its content using constraint, but getting layout warning:

Will attempt to recover by breaking constraint

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in may also be helpful. 2019-03-15 12:27:52.085475+0400 TableCellDynamicHeight[31984:1295380] [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. ( "", "", "", "" )

I checked some threads: Dynamic tableViewCell height

Dynamic Height Issue for UITableView Cells (Swift)

Swift 3 - Custom TableViewCell dynamic height - programatically

What is correct solution, what am I missing?

ViewController:

import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    lazy var tableView: UITableView = {
        let table = UITableView()
        table.backgroundColor = .white
        table.translatesAutoresizingMaskIntoConstraints = false
        table.register(TableViewCell.self, forCellReuseIdentifier: "cellId")
        table.dataSource = self
        table.delegate = self
        return table
    }()


    let arr:[Int:UIColor] = [345: UIColor.random, 422: .random, 23: .random, 344: .random,200: .random,140: .random]

    var pickerDataVisitLocation = [203: "Home", 204: "Hospital", 205: "Other"]

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .red

        self.view.addSubview(tableView)
//
        tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        tableView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
        tableView.tableFooterView = UIView()
    }
}

extension ViewController {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return arr.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! TableViewCell
        let value:UIColor = Array(arr)[indexPath.row].value
        let key = Array(arr)[indexPath.row].key

        cell.setupViews(he: CGFloat(key), color: value)
        return cell
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }

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

extension UIColor {
    static var random: UIColor {
        return UIColor(red: .random(in: 0...1),
                       green: .random(in: 0...1),
                       blue: .random(in: 0...1),
                       alpha: 1.0)
    }
}

TableViewCell:

    import UIKit

    class TableViewCell: UITableViewCell {


        override func awakeFromNib() {
            super.awakeFromNib()


        }

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


        }

        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }

        func setupViews(he:CGFloat, color:UIColor) {

            let v:UIView = UIView()
            v.translatesAutoresizingMaskIntoConstraints = false
            self.addSubview(v)

            v.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
            v.backgroundColor = color
            v.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
            v.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
            v.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
            v.heightAnchor.constraint(equalToConstant: he).isActive = true
            #warning("here is constraint error conflict with bottomAnchor and heightAnchor, need correct solution")
        }

    }

Solution

  • You're doing a couple things wrong...

    First, cells are reused (hence the dequeueReusableCell), but your setupViews() func is adding a new subview every time a cell is reused.

    That means as you scroll, and the cells are reused, you end up with 2, 3, 4 ... a dozen subviews, all with conflicting constraints.

    Move your addSubview() to a common initialization func in your cell, so the view is only created and added once.

    That's also where you should setup your constraints.

    To change the height of the subview as your app is designed, you want to change the .constant on the height constraint of the subview.

    Here is your modified code. I've added enough comments in the code that it should be clear:

    class HattoriTableViewCell: UITableViewCell {
    
        // the view to add as a subview
        let myView: UIView = {
            let v = UIView()
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
    
        // the constraint we'll use for myView's height
        var myViewHeightConstraint: NSLayoutConstraint!
    
        override func awakeFromNib() {
            super.awakeFromNib()
            commonInit()
        }
    
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
    
        func commonInit() -> Void {
    
            // add the subview
            self.addSubview(myView)
    
            // constrain it to all 4 sides
            myView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
            myView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
            myView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
            myView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
    
            // create the height constraint
            myViewHeightConstraint = myView.heightAnchor.constraint(equalToConstant: 1)
    
            // needs Priority less-than 1000 (default) to avoid breaking constraints
            myViewHeightConstraint.priority = UILayoutPriority.init(999)
    
            // activate it
            myViewHeightConstraint.isActive = true
    
        }
    
        func setupViews(he:CGFloat, color:UIColor) {
    
            // set myView's background color
            myView.backgroundColor = color
    
            // change myView's height constraint constant
            myViewHeightConstraint.constant = he
    
        }
    
    }
    
    class HattoriViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
        lazy var tableView: UITableView = {
            let table = UITableView()
            table.backgroundColor = .white
            table.translatesAutoresizingMaskIntoConstraints = false
            table.register(HattoriTableViewCell.self, forCellReuseIdentifier: "cellId")
            table.dataSource = self
            table.delegate = self
            return table
        }()
    
    
        let arr:[Int:UIColor] = [345: UIColor.random, 422: .random, 23: .random, 344: .random,200: .random,140: .random]
    
        var pickerDataVisitLocation = [203: "Home", 204: "Hospital", 205: "Other"]
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.backgroundColor = .red
    
            self.view.addSubview(tableView)
            //
            tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
            tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
            tableView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
            tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
            tableView.tableFooterView = UIView()
    
            // use a reasonable value -- such as the average of what you expect (if known)
            tableView.estimatedRowHeight = 200
        }
    }
    
    extension HattoriViewController {
    
        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return arr.count
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
            let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! HattoriTableViewCell
    
            let value:UIColor = Array(arr)[indexPath.row].value
            let key = Array(arr)[indexPath.row].key
    
            cell.setupViews(he: CGFloat(key), color: value)
    
            return cell
        }
    
        // NOT NEEDED
    //  func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    //      return UITableView.automaticDimension
    //  }
    //
    //  func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    //      return UITableView.automaticDimension
    //  }
    
    }
    
    extension UIColor {
        static var random: UIColor {
            return UIColor(red: .random(in: 0...1),
                           green: .random(in: 0...1),
                           blue: .random(in: 0...1),
                           alpha: 1.0)
        }
    }