iOS
iOS

Reputation: 3616

UITableViewCell doesn't update height after adding a view to UIStackView

I have a UIStackView inside UITableViewCell's contentView. Based on user interaction, I add/remove items in the UIStackView. After modifying the items in UIStackView, I expect the cell to update it's height accordingly. But, it doesn't update it's height unless I call tableView.reloadData(). But, calling reloadData() in cellForRowAtIndexPath / willDisplayCell becomes recursive.

What is the proper way to adjust the cell height at run time based on items in UIStackView?

I use UITableViewAutomaticDimension

Updating the Problem:

Here is a simple prototype of what I am trying to do.

My actual problem is dequeuing the cell.

In the prototype, I have 2 reusable cells and 3 rows. For row 0 and 2, I dequeue cellA and for row 1, I dequeue cellB. Below is the overview on the condition I use.

if indexPath.row == 0 {
    // dequeue cellA with 2 items in stackView
}

if indexPath.row == 1 {
    // dequeue cellB with 25 items in stackView
}

if indexPath.row == 2 {
    // dequeue cellA with 8 items in stackView
}

But the output is, row 0 contains 2 items in stackView - expected row 1 contains 25 items in stackView - expected row 2 contains 2 items in stackView - unexpected, row 0 is dequeued

I also tried removing all arranged subViews of stackView in cellForRowAtIndexPath. But, doing so, flickers the UI when scrolling. How can I manage to get the desired output?

Upvotes: 1

Views: 3912

Answers (2)

DonMag
DonMag

Reputation: 77486

I believe the problem is when you are adding views to the stack view.

In general, adding elements should take place when the cell is initialized.

willDisplay cell: is where one handles modifying attributes of cell contents.

If you move your code from willDisplay cell: to cellForRowAt indexPath: you should see a big difference.

I just made that one change to the code you linked to, and the rows are now auto-sizing based on the stack view contents.

Edit: Looked at your updated code... the issue was still that you are adding your arrangedSubviews in the wrong place. And you compound it by calling reloadData().

Second Edit: Forgot to handle previously added subviews when the cells are reused.

Updated code... replace your ViewController code with:

//
//  ViewController.swift
//

import UIKit

class ViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.tableFooterView = UIView()
        tableView.estimatedRowHeight = 56
    }

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

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

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

        var cell = UITableViewCell()

        if indexPath.row == 0 || indexPath.row == 2 {
            cell = tableView.dequeueReusableCell(withIdentifier: "cell")!

            if let stackView = cell.viewWithTag(999) as? UIStackView {
                let numberOfItemsInStackView = (indexPath.row == 0) ? 2 : 8
                let color = (indexPath.row == 0) ? UIColor.gray : UIColor.black

                // cells are reused, so clear out any previously added subviews...
                // but leave the first view that is part of the cell prototype
                while stackView.arrangedSubviews.count > 1 {
                    stackView.arrangedSubviews[1].removeFromSuperview()
                }

                // use "i" so we can count
                for i in 1...numberOfItemsInStackView {

                    // use label instead of view so we can number them for testing
                    let newView = UILabel()
                    newView.text = "\(i)"
                    newView.textColor = .yellow

                    // add a border, so we can see the frames
                    newView.layer.borderWidth = 1.0
                    newView.layer.borderColor = UIColor.red.cgColor

                    newView.backgroundColor = color
                    let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 54)
                    heightConstraint.priority = 999
                    heightConstraint.isActive = true
                    stackView.addArrangedSubview(newView)
                }
            }

        }

        if indexPath.row == 1 {
            cell = tableView.dequeueReusableCell(withIdentifier: "lastCell")!

            if let stackView = cell.viewWithTag(999) as? UIStackView {
                let numberOfItemsInStackView = 25

                // cells are reused, so clear out any previously added subviews...
                // but leave the first view that is part of the cell prototype
                while stackView.arrangedSubviews.count > 1 {
                    stackView.arrangedSubviews[1].removeFromSuperview()
                }

                // use "i" so we can count
                for i in 1...numberOfItemsInStackView {

                    // use label instead of view so we can number them for testing
                    let newView = UILabel()
                    newView.text = "\(i)"
                    newView.textColor = .yellow

                    // add a border, so we can see the frames
                    newView.layer.borderWidth = 1.0
                    newView.layer.borderColor = UIColor.red.cgColor

                    newView.backgroundColor = UIColor.darkGray
                    let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 32)
                    heightConstraint.priority = 999
                    heightConstraint.isActive = true
                    stackView.addArrangedSubview(newView)
                }
            }

        }

        return cell

    }

//    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
//        if cell.reuseIdentifier == "cell" {
//            if let stackView = cell.viewWithTag(999) as? UIStackView {
//                let numberOfItemsInStackView = (indexPath.row == 0) ? 2 : 8
//                let color = (indexPath.row == 0) ? UIColor.gray : UIColor.black
//                guard stackView.arrangedSubviews.count == 1 else { return }
//                for _ in 1...numberOfItemsInStackView {
//                    let newView = UIView()
//                    newView.backgroundColor = color
//                    let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 54)
//                    heightConstraint.priority = 999
//                    heightConstraint.isActive = true
//                    stackView.addArrangedSubview(newView)
//                }
//                tableView.reloadData()
//            }
//        }
//        
//        if cell.reuseIdentifier == "lastCell" {
//            if let stackView = cell.viewWithTag(999) as? UIStackView {
//                let numberOfItemsInStackView = 25
//                guard stackView.arrangedSubviews.count == 1 else { return }
//                for _ in 1...numberOfItemsInStackView {
//                    let newView = UIView()
//                    newView.backgroundColor = UIColor.darkGray
//                    let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 32)
//                    heightConstraint.priority = 999
//                    heightConstraint.isActive = true
//                    stackView.addArrangedSubview(newView)
//                }
//                tableView.reloadData()
//            }
//        }
//    }

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

Upvotes: 2

GaétanZ
GaétanZ

Reputation: 4930

Try to reload only the cell using: https://developer.apple.com/documentation/uikit/uitableview/1614935-reloadrows

Example code

Here is an example. We have basic table view cells (TableViewCell) inside a view controller. The cells have 2 labels inside a stack view. We can hide or show the second label using the collapse/reveal methods.

class TableViewCell : UITableViewCell {
    @IBOutlet private var stackView: UIStackView!
    @IBOutlet private var firstLabel: UILabel!
    @IBOutlet private var secondLabel: UILabel!

    func collapse() {
        secondLabel.isHidden = true
    }

    func reveal() {
        secondLabel.isHidden = false
    }
}

class ViewController : UIViewController {

    @IBOutlet var tableView: UITableView!

    fileprivate var collapsedCells: Set<IndexPath> = []

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.estimatedRowHeight = 128
    }

    @IBAction private func buttonAction(_ sender: Any) {
        collapseCell(at: IndexPath(row: 0, section: 0))
    }

    private func collapseCell(at indexPath: IndexPath) {
        if collapsedCells.contains(indexPath) {
            collapsedCells.remove(indexPath)
        } else {
            collapsedCells.insert(indexPath)
        }
        tableView.reloadRows(at: [indexPath], with: .automatic)
    }
}


extension ViewController : UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableViewCell
        if collapsedCells.contains(indexPath) {
            cell.collapse()
        } else {
            cell.reveal()
        }
        return cell
    }

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

Upvotes: 2

Related Questions