Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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")
        }

    }
like image 320
Hattori Hanzō Avatar asked Oct 24 '25 15:10

Hattori Hanzō


2 Answers

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)
    }
}
like image 105
DonMag Avatar answered Oct 26 '25 05:10

DonMag


In your situation height is available within dataSource arr, so you do not need:

  1. Height constraint
  2. estimatedHeightForRowAtIndexPath

All you need is to return actual height in heightForRowAtIndexPath, but first your dataSource arr:[Int:UIColor] is an Dictionary and I will not rely on its order, lets change it to an Array of Tuples:

var dataSource: [(height: CGFloat, color: UIColor)] = [
    (345, .random),
    (422, .random),
    (23, .random),
    (344, .random),
    (200, .random),
    (140, .random)
]

Now use following UITableView Delegate/DataSource methods:

extension ViewController: UITableViewDataSource, UITableViewDelegate {

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

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return dataSource[indexPath.row].height
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! TableViewCell
        cell.setupViews(color: dataSource[indexPath.row].color)
        return cell
    }

}

Since you do not need height constraint, I removed he parameter from setupViews method

like image 45
AamirR Avatar answered Oct 26 '25 04:10

AamirR



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!