4

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

    }
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Hattori Hanzō
  • 2,349
  • 4
  • 19
  • 36
  • 1
    Why do you want to use dynamic height with constraints if you already know the exact height for each item? – Lukas Würzburger Mar 15 '19 at 08:52
  • fist remove `func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableView.automaticDimension } ` – SPatel Mar 15 '19 at 09:18
  • 1
    warning so simple either set bottom anchor or height anchor don't set both if you height as greater than equal no need to set bottom constraint – Vinodh Mar 15 '19 at 10:03
  • @Vinodh not works, if I user only `bottomAnchor` or `heightAnchor ` you can test it – Hattori Hanzō Mar 15 '19 at 10:10

3 Answers3

6

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)
    }
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Setting `heightConstraint.priority` to lower than 1000 solved my issue. Thanks! – Baran Aug 06 '19 at 10:30
  • What if we have not only one view but 8-10 resizing views and we need the final value for row height as a result? When I try to do so, content overlays the next cell (not resized) – J A S K I E R Apr 01 '20 at 18:35
  • 1
    @Oleksandr - as long as you have a complete, valid chain of vertical constraints, auto-layout will size your cell correctly. If you are getting content "overlaying the next cell" then there is something wrong with your constraints. – DonMag Apr 01 '20 at 20:02
  • The trick for me was setting the height constraint in the `commonInit` method instead of `updateViewConstraints` method with all the other constraints. Thanks. – App Dev Guy Nov 30 '20 at 03:39
2

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

AamirR
  • 11,672
  • 4
  • 59
  • 73
  • Thanks for advice but my goal is to get `UITableViewCell` dynamic height by content, for example load image from cloud or put `UICollectionView` – Hattori Hanzō Mar 15 '19 at 13:02
0

I had the same problem but for a different reason, that I would like to share with you.

I simply override the layoutSubviews() method to add my custom layouts.

but then I didn't call the layoutIfNeeded() method in the initializer to activate them, rather the layouts were only activated when the cell was dequeued and reused.

here is my code for reference if you face the same issue:

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

    contentView.addSubviews(containerView)
    containerView.addSubviews(titleLabel, subtitleLabel, activteSwitch)

    layoutIfNeeded() // Required to triger the overriden layoutSubviews() upon initialization
}

// However, I shouldn't override this method or add any constraints here
override func layoutSubviews() {
    let margin: CGFloat = 8

    containerView.snapToEdges()
    NSLayoutConstraint.activate([
        activteSwitch.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -margin),
        activteSwitch.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),

        titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: margin),
        titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: margin),
        titleLabel.trailingAnchor.constraint(equalTo: activteSwitch.leadingAnchor, constant: -margin),

        subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: margin),
        subtitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: margin),
        subtitleLabel.trailingAnchor.constraint(equalTo: activteSwitch.leadingAnchor, constant: -margin),
        subtitleLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -margin)

    ])
}
Wissa
  • 1,444
  • 20
  • 24