3

I want the cells of a UITableView to adapt to the size of their content in iOS 10 and 11 with:

tableView.estimatedRowHeight = UITableViewAutomaticDimension // default in iOS 11
tableView.rowHeight = UITableViewAutomaticDimension

Without setting the tableView.rowHeight to an explicit numerical value which is the new default in iOS 11.

A UIView has no intrinsic content size, therefore I set a layout constraint for its height anchor. However that anchor breaks during run time.

What internal constraints in the UITableViewCell are necessary for the cell to adapt to its content?


This works in iOS 11 only:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        let tableView = UITableView()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.estimatedRowHeight = UITableViewAutomaticDimension
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.delegate = self
        tableView.dataSource = self
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)

        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
            tableView.rightAnchor.constraint(equalTo: view.rightAnchor)
            ])
    }

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

        let cell = UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: "cell")

        let view = UIView()
        view.backgroundColor = .blue
        view.translatesAutoresizingMaskIntoConstraints = false
        cell.contentView.addSubview(view)

        NSLayoutConstraint.activate([
            view.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
            view.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
            view.leftAnchor.constraint(equalTo: cell.contentView.leftAnchor),
            view.rightAnchor.constraint(equalTo: cell.contentView.rightAnchor),
            view.heightAnchor.constraint(equalToConstant: 300)
            ])

        return cell
    }

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

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

In iOS 10 it throws this runtime error and the cell size doesn't adapt:

[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:0x17009c020 V:|-(0)-[test.MyView:0x11dd1acb0] (active, names: '|':UITableViewCellContentView:0x11dd15220 )", "NSLayoutConstraint:0x17009c160 test.MyView:0x11dd1acb0.bottom == UITableViewCellContentView:0x11dd15220.bottom (active)", "NSLayoutConstraint:0x17009c390 test.MyView:0x11dd1acb0.height == 300 (active)", "NSLayoutConstraint:0x17009be40 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x11dd15220.height == 43.6667 (active)" )

Will attempt to recover by breaking constraint NSLayoutConstraint:0x17009c390 test.MyView:0x11dd1acb0.height == 300 (active)

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in UIKit/UIView.h may also be helpful.


This works in iOS 10 and 11 but does not use the new iOS 11 approach as tableView.estimatedRowHeight is not UITableViewAutomaticDimension:

class ViewController_TableView: UIViewController, UITableViewDataSource, UITableViewDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        let tableView = UITableView()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.estimatedRowHeight = 30
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.delegate = self
        tableView.dataSource = self
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)

        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
            tableView.rightAnchor.constraint(equalTo: view.rightAnchor)
            ])
    }

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

        let cell = UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: "cell")

        let view = MyView()
        view.backgroundColor = .blue
        view.translatesAutoresizingMaskIntoConstraints = false
        cell.contentView.addSubview(view)

        NSLayoutConstraint.activate([
            view.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
            view.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
            view.leftAnchor.constraint(equalTo: cell.contentView.leftAnchor),
            view.rightAnchor.constraint(equalTo: cell.contentView.rightAnchor)
            ])

        return cell
    }

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

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

class MyView: UIView {

    override var intrinsicContentSize: CGSize {
        get {
            return CGSize(width: 300, height: 300)
        }
    }
}
Manuel
  • 14,274
  • 6
  • 57
  • 130
  • here are a few working examples: https://www.raywenderlich.com/129059/self-sizing-table-view-cells, https://stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights; the actual feature expects you to set up all vertical constraints from top of the cell to the bottom, if you missed to do that in IB. – holex Oct 03 '17 at 10:16
  • @holex I want to know specifically why the simple code example in my post doesn't work. The code works when `rowHeight` is not set to `UITableViewAutomaticDimension` which is not how it is supposed to be done in iOS 11. Your reference is a pre-iOS 11 example. – Manuel Oct 03 '17 at 10:21
  • 1
    on iOS11, if you set the height's constraint's __priority__ to e.g. `999.9`, that will resolve the _breaking-in-runtime_ issue immediately; but it is not actually an explanation of why the constraint breaks at the first place... _(it can be related to the representation of, or rounding floating values, but that is a wild speculation from me)_, however your example is also quite hypothetical as no one actually generates the UI like this in practice, so probably no one actually needs to face to such issue, like ever. – holex Oct 03 '17 at 11:21
  • @holex Why do you say "no one actually generates the UI like this"? Can you please have another look at my question? I think my initial post was a bit confusing. – Manuel Oct 03 '17 at 11:23
  • because it is highly anti-pattern in commercial environment; like. e.g. if you need to generate a fairly complex UI, and later maintain it or change it, that'd take much longer time than having a prototype cell in IB and make the changes in the editor (especially if your app uses e.g. 25 prototype cells) – I need to admit that it is great fun to play with the UI like this, but if anyone does it in _real_ commercial environment that person would be seriously incompetent. no offence, of course :) – holex Oct 03 '17 at 11:31
  • @holex Got it, the example is of course stripped down to focus on the issue. The cell may be subclassed and add the UIView by itself, but that shouldn't make any difference for its behavior in this example. On a personal note, I try not to use IB whenever possible because I think manual code provides better maintainability. – Manuel Oct 03 '17 at 11:44
  • it makes a little difference actually from one particular perspective: when the cell is loaded from a `xib` file, the predefined constraints are loaded already correctly during the init, but when you created the cell programatically there are no constraints in the cell at all after initialisation finished so iOS adds the default constraints to the cell and those seems interfering the constraints which you try to add _after_ it – and that might be a straight answer to your concerns: why the UI is not supposed to build up like this because, in your own example, the maintainability is much worse. – holex Oct 03 '17 at 11:51
  • don't get me wrong, I'm not anyone's supervisor here at all :) but I hardly imagine anyone in my team could prove your statement about the _"better maintainability"_ of such UI – as its time-complexity is provable worse. – holex Oct 03 '17 at 11:55
  • @holex I think whatever can be done in IB *must* also be possible in code. If not that would be a bug. What I posted is a primitive example to demo the issue. IB is out of question here. And I appreciate your comments, reflection is always good :) Also I have now added another code example to make the issue clearer. – Manuel Oct 03 '17 at 12:01
  • that is true, you'd be able to do it on both ways :) but the ability does not mean both ones are practical choices... I don't intend turning the knife in you but only concluding my thinking: we have spend a whole morning to resolve an issue what would not have been appeared at all, if you'd used IB – so, I could say it was a waste of resources, because another developer might have been able to finish the entire UI in the same time while someone is struggling finding solutions in the illusion of _"better maintainability"_ :) that is not really good in any commercial environment :( – holex Oct 03 '17 at 12:13
  • 1
    @holex, A developer who flees into IB because he doesn't understand iOS layout mechanisms has an entirely different problem. Comparing commercial efficiency of various development concepts is an interesting topic, but I am here to find an answer to my question. – Manuel Oct 03 '17 at 12:19
  • yes, and it is true from this perspective as well: a developer who _thinks_ if they create the UI in code that they _seem_ understanding the view-lifecycle is also a real problem. btw, I'm glad you find your solution :) – holex Oct 03 '17 at 12:48
  • I had the same issue in my app, where I built everything in the storyboard (not code), and in iOS 10- it was working fine, no constraint warnings for my dummy view that sits inside the dynamic height cell to set its "min height". But as soon as I compiled against iOS 11, it started throwing warnings. Setting the dummy view's height constraint priority to 999 fixed the warning as @holex's comment suggested. – KBog Jan 10 '18 at 03:49

1 Answers1

0

you are not conform the delegate to your controller. then use the code

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

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

    let params : [String: Any] = ["q": "dd", "format": "json", "pretty": 1,"no_html": 1, "skip_disambig": 1]
    Alamofire.request("https://api.duckduckgo.com", method: .get, parameters: params).responseJSON { (responseData) in
        print(responseData)
    }

    let tableView = UITableView()
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    tableView.estimatedRowHeight = 40
    tableView.delegate = self
    tableView.dataSource = self
    tableView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(tableView)

    NSLayoutConstraint.activate([
        tableView.topAnchor.constraint(equalTo: view.topAnchor),
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
        tableView.rightAnchor.constraint(equalTo: view.rightAnchor)
        ])

}

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

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

    let cell = UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: "cell")

    let view = UIView()
    view.backgroundColor = .blue
    view.translatesAutoresizingMaskIntoConstraints = false
    cell.contentView.addSubview(view)

    NSLayoutConstraint.activate([
        view.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
        view.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
        view.leftAnchor.constraint(equalTo: cell.contentView.leftAnchor),
        view.rightAnchor.constraint(equalTo: cell.contentView.rightAnchor),
        view.heightAnchor.constraint(equalToConstant: 300) // this breaks on run time
        ])

    return cell
}

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

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 1
}
// use this delegate 
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return UITableViewAutomaticDimension
}


}
  • 1
    The delegate method should not be necessary if the property is set. It's even discouraged to be used due to performance implications for table views with large number of cells. – Manuel Oct 03 '17 at 10:16
  • i agree with you. but i think if you not use this delegate method, then you need set the content hugging or content compression priority. – Asaduzzaman Shuvro Oct 03 '17 at 10:20
  • Can you repost my code with the necessary content hugging or content compression priorities? I doubt that has anything to do with it. The delegate function only overrides the property. – Manuel Oct 03 '17 at 10:23
  • I just notices you set `tableView.estimatedRowHeight = 40` please read my post again, I am referring specifically to iOS 11 where that property is `UITableViewAutomaticDimension´ by default. – Manuel Oct 03 '17 at 10:25
  • i checked your code to xcode 9 and ios11 simulator. its working perfectly. and notice that uiview has intrinsic content. you have to use uilabel or uimage. then i think it should be worked perfectly. :) – Asaduzzaman Shuvro Oct 03 '17 at 10:57
  • Yes, I clarified my question, I am looking for a code that works in iOS 10 and 11, but with the approach of `UITableViewAutomaticDimension`. My old code worked in iOS 10 and not in iOS 11 because the default of `tableView.estimatedRowHeight` changed. Now I am trying to adapt the code to that approach but it doesn't work in iOS 10. – Manuel Oct 03 '17 at 11:06
  • I rephrased my question to reflect the issue clearer. – Manuel Oct 03 '17 at 11:18