jshapy8 jshapy8 - 1 month ago 8
Swift Question

iOS/Swift: Dynamic UITableViewCell row height with embedded UITableView not working

I have a

UITableViewCell
which contains a
UICollectionView
on top and a
UITableView
on the bottom. The idea is that a dynamic amount of cells will be created in the inner
UITableView
and the parent
UITableViewCell
that encloses the two subviews will increase its height proportionally.

I am trying to take advantage of the
estimatedRowHeight
+
UITableViewAutomaticDimention
feature of the
UITableViewCell
that will allow the cell height to increase dynamically. However, it is not working. It completely removes the embedded
UITableView
from view.

Comparison of implementations

I have not made any constraints that limit the height of the enclosed
UITableView
, so I am not sure why it is not working.

Contraints

Here is the implementation that attempts to make a dynamically sized
UITableViewCell
:

class OverviewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Enclosed Table View Example"
}

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

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

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 325 // Height for inner table view with 1 cell
}

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

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 45
}

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "appHeaderCell") as! AppHeaderCell

return cell
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "appCell", for: indexPath) as! AppCell

return cell
}
}


My only guess is that the constraint
bottom = Inner Table View.bottom + 7
is causing the issue, but the entire view falls apart when this constraint is removed.

What can I do to make the complex outer
UITableViewCell
dynamically adjust height based on the number of cells in the embedded
UITableView
?

Answer Source

Although it may seem like a good idea, the use of UITableViewAutomaticDimension in conjunction with estimatedRowHeight is not good to use in scenarios like this where we have general content inside table view cells. Making use of the heightForRowAt method, you can calculate the size of each individual cell before it centers the table.

Once we know how many cells will be in the inner table, you need to create an array whose elements correspond to the number inner cells that will ultimately determine the height of the outer cell, as all other content is constant.

let cellListFromData: [CGFloat] = [3, 1, 4]

This array will give us the number of sections in our outer table view:

func numberOfSections(in tableView: UITableView) -> Int {
    return cellListFromData.count
}

We will convert each element in this array to a cell height in the following way:

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    let prototypeCell = tableView.dequeueReusableCell(withIdentifier: "appCell") as! AppCell

    let baseHeight = betweenCellSpacing + prototypeCell.innerCollectionView.contentSize.height + prototypeCell.innerTableView.sectionHeaderHeight + outerTableView.sectionHeaderHeight
    let dynamicHeight = prototypeCell.innerTableView.contentSize.height - prototypeCell.innerTableView.sectionHeaderHeight

    return baseHeight + (dynamicHeight * cellListFromData[indexPath.section])
}

That is, inside of the heightForRowAt method, we dequeue a prototype cell that will not be used in the resulting view (as dequeueReusableCell is not called inside cellForRowAt in this case). We use this prototype cell to extract information about what is constant and what is dynamic about the cell's content. The baseHeight is the accumulated height of all the constant elements of the cell (plus the between-cell spacing) and the dynamicHeight is the height of an inner UITableViewCell. The height of each cell then becomes baseHeight + dynamicHeight * cellListFromData[indexPath.section].

Next, we add a numberOfCells variable to the class for the custom cell and set this in the cellForRowAt method in the main table view:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "appCell", for: indexPath) as! AppCell

    cell.numberOfCells = Int(cellListFromData[indexPath.section])
    cell.innerTableView.reloadData()

    return cell
}

numberOfCells is set with the same cellListFromData that we used to get the height of the cell. Also, it is critical to call reloadData() on the inner table view after setting its number of cells so that we see that update in the UI.

Here is the full code:

class OverviewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    @IBOutlet weak var outerTableView: UITableView!
    let cellSpacing: CGFloat = 25
    let data: [CGFloat] = [3, 1, 4]

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return data.count
    }

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

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let prototypeCell = tableView.dequeueReusableCell(withIdentifier: "appCell") as! AppCell

        let baseHeight = cellSpacing + prototypeCell.innerCollectionView.contentSize.height + prototypeCell.innerTableView.sectionHeaderHeight + outerTableView.sectionHeaderHeight
        let dynamicHeight = prototypeCell.innerTableView.contentSize.height - prototypeCell.innerTableView.sectionHeaderHeight

        return baseHeight + (dynamicHeight * data[indexPath.section])
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "appCell", for: indexPath) as! AppCell

        cell.numberOfCells = Int(data[indexPath.section])
        cell.innerTableView.reloadData()

        return cell
    }
}

class AppCell: UITableViewCell, UITableViewDataSource, UITableViewDelegate {
    @IBOutlet weak var innerCollectionView: UICollectionView!
    @IBOutlet weak var innerTableView: UITableView!
    var numberOfCells: Int = 0

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

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

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let cell = tableView.dequeueReusableCell(withIdentifier: "featureHeaderCell") as! BuildHeaderCell

        return cell
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "innerCell", for: indexPath) as! InnerCell

        return cell
    }
}

Methods relating to configuring the inner collection view is not included here as it is not related to the problem.