uitableviewuiviewuiviewcontrollerautolayoutuiview-hierarchy

A TableView that "fits" (ala "sizeToFit") its dynamic cell's content size. Then nested in a parent UIView that is also "sized to fit"


I'm not sure if I'm approaching this in the most optimal way.

(1) First, I want a tableView with an arbitrary number of cells, whose dynamic cell height varies (based on content from web server, cannot be calculated ahead of time). But I want the tableView's height to only grow as much to fit all the cell's content (not any taller or shorter). This table view will NOT be scrollable (it's parent's parent view will be).

(2) Then, I want the tableView nested inside a parent UIView.

I mostly got (1) working, although I don't know if my method is optimal or hackish. But I definitely don't have (2) working :(

(Pic below) Turqoise view is the parent of the table View. Cells (in red) and the table size (in white) is correct, but the parent view is shorter than it should be.

Screen Shot 2017-07-07 at 9.08.45 PM.png

After my code change, I have the opposite problem. The parent height as grown to about the right height, but the tableView got chopped off.

Screen Shot 2017-07-07 at 9.10.06 PM.png

This is the code change:

Screen Shot 2017-07-07 at 9.22.31 PM.png

View hierarchy:

Screen Shot 2017-07-07 at 9.11.06 PM.png

If I don't try to put the tableView in a parent UIView, the table "size to fits" just right:

Screen Shot 2017-07-08 at 10.02.57 AM.png

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    @IBOutlet var tableView: UITableView!
    @IBOutlet var tableViewHeightConstraint: NSLayoutConstraint!

    @IBOutlet var parentView: UIView!

    @IBOutlet var parentViewHeightConstraint: NSLayoutConstraint!

    let data = ["this is a short line.",
                "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint. deserunt mollit anim id est laborum.",
                "another short line.",
                "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam  explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia."
                ]

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.delegate = self
        tableView.dataSource = self

        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.estimatedRowHeight = 50

    } // viewDiDload
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        // If I add this new frame for the table's parent view,
        // the table view height gets chopped off. See 3 lines below
        var newFrameForParent = self.parentView.frame
        newFrameForParent.size.height = self.tableView.contentSize.height
        self.parentView.frame = newFrameForParent
        // But if I comment out the above 3 lines, the table view
        // height is fine (table height fits actual dynamic content size)
        // but the table's parent view is shorter than the table view

        var newFrame = self.tableView.frame
        newFrame.size.height = self.tableView.contentSize.height
        self.tableView.frame = newFrame

        print("viewDidAppear: tableView contentSize w: \(tableView.contentSize.width) h: \(tableView.contentSize.height)")

    } // viewDidAppear

    override func viewWillLayoutSubviews() {
        super.updateViewConstraints()
        self.tableViewHeightConstraint.constant = self.tableView.contentSize.height
        //self.parentViewHeightConstraint.constant = self.tableView.contentSize.height + 20
        print("viewWillLayoutSubviews: tableView contentSize w: \(tableView.contentSize.width) h: \(tableView.contentSize.height)")
    } // viewWillLayoutSubviews
    override func viewWillAppear(_ animated: Bool) {
        //tableView.sizeToFit()
    }

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

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellID", for: indexPath) as! CustomCell
        cell.cellLabel?.text = data[indexPath.row]

        print("cell \(indexPath.row) height is: \(cell.frame.height)")

        return cell
    }

}

The full code base is here: https://github.com/jayliew/UITableViewSizeToFitDynamicHeightCells/tree/master/TableViewFitContent

I'm not sure what I need to know, that I don't know, but I suspect it's something around fully understanding how views are laid out, how AutoLayout comes into play, with the viewController lifecycles like viewWillAppear, viewWillLayoutSubviews, etc. (how each stage impacts the laying out of views, with or without AutoLayout). I don't just want to solve this problem, but understand the architecture more holistically and would appreciate it if someone could point me in the right direction.


Solution

  • Answering my own question:

    The lesson I learned was not to mix AutoLayout with manually creating frames and changing views to using those frames.

    I learned that I had to first artificially set the tableView to a ginormous height, e.g. 1000, so that the dynamic cells would be "visible" on the table. Then only can I loop through all those cells, get the frame height, manually add them all up, before setting the AutoLayout height constraint's constant on the tableView to equal the sum of all the cell's height.

    Also, the above had to happen inside of viewDidAppear()

    That was it! Sample code here: https://github.com/jayliew/UITableViewSizeToFitDynamicHeightCells

    Thanks to @ddustin on Github

    Screen Shot 2017-07-13 at 1.45.38 PM.png

    Screen Shot 2017-07-13 at 1.45.56 PM.png