0

I am working on a project needs to add a UICollectionView(horizontal direction) inside UITableViewCell. The UITableViewCell height is using UITableViewAutoDimension and each UITableViewCell I am having a UIView(with a border for design requirements) as a base view, and in the UIView, I have a UIStackView added in as the containerView to proportionally fill the UICollectionView with two other buttons vertically. And then for UICollectionViewCell, I have added in a UIStackView to fill five labels.

Now, the auto-layout works if the UITableViewDelegate assigns a fixed height. But it doesn't work with the UITableViewAutoDimension. My guessing is that UICollectionView frame is not ready while UITableView is rendering its' cells. So the UITableViewAutoDimension calculates the UITableViewCell height with a default height of the UICollectionView which is zero.

So, of course, I have been searching before I throw a question out on the Internet but no solutions worked for me. Here are some links I have tried.

UICollectionView inside a UITableViewCell — dynamic height?

Making UITableView with embedded UICollectionView using UITableViewAutomaticDimension

UICollectionView inside UITableViewCell does NOT dynamically size correctly

Does anyone have the same issue? If the links above did work, please feel free to let me know in case I did it wrong. Thank you

--- Updated Sep 23th, 2018:

  • Layout Visualization: There are some UI modifications, but it does not change the issue that I am facing. Hope the picture can help.

    enter image description here

  • Code: The current code I have is actually not using the UIStackView in UITableViewCell and the UITableView heightForRowAtIndex I return a fixed height with 250.0. However, UITableView won't configure the cell height properly if I return UITableViewAutoDimension as I mentioned in my question.

1. UITableViewController

class ViewController: UIViewController {

    private let tableView: UITableView = {
       let tableView = UITableView()
       tableView.translatesAutoresizingMaskIntoConstraints = false
       tableView.register(ViewControllerTableViewCell.self, forCellReuseIdentifier: ViewControllerTableViewCell.identifier)
       return tableView
    }()

    private lazy var viewModel: ViewControllerViewModel = {
        return ViewControllerViewModel(models: [
            Model(title: "TITLE", description: "SUBTITLE", currency: "USD", amount: 100, summary: "1% up"),
            Model(title: "TITLE", description: "SUBTITLE", currency: "USD", amount: 200, summary: "2% up"),
            Model(title: "TITLE", description: "SUBTITLE", currency: "USD", amount: 300, summary: "3% up"),
        ])
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.dataSource = self
        tableView.delegate = self

        view.addSubview(tableView)

        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        // Do any additional setup after loading the view, typically from a nib.
    }

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

extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return viewModel.numberOfSections
    }

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

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: ViewControllerTableViewCell.identifier, for: indexPath) as! ViewControllerTableViewCell
        cell.configure(viewModel: viewModel.cellViewModel)
        return cell
    }
}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 250.0
    }
}

2. UITableViewCell

class ViewControllerTableViewCell: UITableViewCell {

    static let identifier = "ViewControllerTableViewCell"

    private var viewModel: ViewControllerTableViewCellViewModel!

    private let borderView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.layer.borderColor = UIColor.black.cgColor
        view.layer.borderWidth = 1
        return view
    }()

    private let stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.distribution = .fillProportionally
        return stackView
    }()

    private let seperator: UIView = {
        let seperator = UIView()
        seperator.translatesAutoresizingMaskIntoConstraints = false
        seperator.backgroundColor = .lightGray
        return seperator
    }()

    private let actionButton: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitle("Show business insight", for: .normal)
        button.setTitleColor(.black, for: .normal)
        return button
    }()

    private let pageControl: UIPageControl = {
        let pageControl = UIPageControl()
        pageControl.translatesAutoresizingMaskIntoConstraints = false
        pageControl.pageIndicatorTintColor = .lightGray
        pageControl.currentPageIndicatorTintColor = .black
        pageControl.hidesForSinglePage = true
        return pageControl
    }()

    private let collectionView: UICollectionView = {
        let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        let collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: 200, height: 200), collectionViewLayout: layout)
        collectionView.isPagingEnabled = true
        collectionView.backgroundColor = .white
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.register(ViewControllerCollectionViewCell.self, forCellWithReuseIdentifier: ViewControllerCollectionViewCell.identifier)
        return collectionView
    }()

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setUpConstraints()
        setUpUserInterface()
    }

    func configure(viewModel: ViewControllerTableViewCellViewModel) {
        self.viewModel = viewModel
        pageControl.numberOfPages = viewModel.items.count
        collectionView.reloadData()
    }

    @objc func pageControlValueChanged() {
        let indexPath = IndexPath(item: pageControl.currentPage, section: 0)
        collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
    }

    private func setUpConstraints() {
        contentView.addSubview(borderView)
        borderView.addSubview(actionButton)
        borderView.addSubview(seperator)
        borderView.addSubview(pageControl)
        borderView.addSubview(collectionView)

        NSLayoutConstraint.activate([
            borderView.topAnchor.constraint(equalTo: topAnchor, constant: 10),
            borderView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            borderView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
            borderView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10),
        ])

        NSLayoutConstraint.activate([
            actionButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
            actionButton.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
            actionButton.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
            actionButton.bottomAnchor.constraint(equalTo: borderView.bottomAnchor)
        ])

        NSLayoutConstraint.activate([
            seperator.heightAnchor.constraint(equalToConstant: 1),
            seperator.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
            seperator.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
            seperator.bottomAnchor.constraint(equalTo: actionButton.topAnchor)
        ])

        NSLayoutConstraint.activate([
            pageControl.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
            pageControl.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
            pageControl.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
            pageControl.bottomAnchor.constraint(equalTo: seperator.topAnchor)
        ])

        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: borderView.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: pageControl.topAnchor)
        ])
    }

    private func setUpUserInterface() {
        selectionStyle = .none
        collectionView.delegate   = self
        collectionView.dataSource = self
        pageControl.addTarget(self, action: #selector(pageControlValueChanged), for: .valueChanged)
    }
}

extension ViewControllerTableViewCell: UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return viewModel!.numberOfSections
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return viewModel!.numberOfRowsInSection
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ViewControllerCollectionViewCell.identifier, for: indexPath) as! ViewControllerCollectionViewCell
        let collectionCellViewModel = viewModel!.collectionCellViewModel(at: indexPath)
        cell.configure(viewModel: collectionCellViewModel)
        return cell
    }
}

extension ViewControllerTableViewCell: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        debugPrint("did select \(indexPath.row)")
    }

    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        pageControl.currentPage = indexPath.row
    }
}

extension ViewControllerTableViewCell: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: collectionView.frame.width - 40.0, height: collectionView.frame.height)
    }

    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: 20.0, bottom: 0, right: 20.0)
    }

    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 40.0
    }
}

3. UICollectionViewCell

class ViewControllerCollectionViewCell: UICollectionViewCell {

    override class var requiresConstraintBasedLayout: Bool {
        return true
    }

    static let identifier = "ViewControllerCollectionViewCell"

    private let stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.distribution = .fillEqually
        return stackView
    }()

    private let titleLabel: UILabel = {
        let titleLabel = UILabel()
        titleLabel.textColor = .black
        titleLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        return titleLabel
    }()

    private let descriptionLabel: UILabel = {
        let descriptionLabel = UILabel()
        descriptionLabel.textAlignment = .right
        descriptionLabel.textColor = .black
        descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
        return descriptionLabel
    }()

    private let amountLabel: UILabel = {
        let amountLabel = UILabel()
        amountLabel.textColor = .black
        amountLabel.textAlignment = .right
        amountLabel.translatesAutoresizingMaskIntoConstraints = false
        return amountLabel
    }()

    private let summaryLabel: UILabel = {
        let summaryLabel = UILabel()
        summaryLabel.textColor = .black
        summaryLabel.textAlignment = .right
        summaryLabel.translatesAutoresizingMaskIntoConstraints = false
        return summaryLabel
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.addSubview(stackView)
        stackView.addArrangedSubview(titleLabel)
        stackView.addArrangedSubview(descriptionLabel)
        stackView.addArrangedSubview(amountLabel)
        stackView.addArrangedSubview(summaryLabel)

        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: topAnchor),
            stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
            stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func configure(viewModel: CollectionCellViewModel) {
        titleLabel.text = viewModel.title
        descriptionLabel.text = viewModel.description
        amountLabel.text = viewModel.amount.localizedCurrencyString(with: viewModel.currency)
        summaryLabel.text = viewModel.summary
    }
}
Tim Lin
  • 103
  • 1
  • 9

1 Answers1

0

To enable self-sizing table view cells, you must set the table view’s rowHeight property to UITableViewAutomaticDimension. You must also assign a value to the estimatedRowHeight property, you also need an unbroken chain of constraints and views to fill the content view of the cell.

More: https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSelf-SizingTableViewCells.html#//apple_ref/doc/uid/TP40010853-CH25-SW1

So in this case, the UIView with border determines the table view cell height dynamically, you have to tell the system how tall this UIView is, you can constraint this view to the UIStackView edges (top, bottom, leading, trailing), and use the UIStackView intrinsic content height as the UIView(with border) height.

The trouble is in the UIStackView intrinsic content height and with the distribution property. The UIStackView can estimate a height automatically for the two UIButtons (based on the size of text, the font style, and the font size), but the UICollectionView has no intrinsic content height, and since your UIStackview is filled proportionally the UIStackView sets a height of 0.0 for the UICollectionView, so it looks like something like this:

Fill Proportionally UIStackView

But if you change the UIStackView distribution property to fill equally you will have something like this:

Fill Equally UIStackView

If you want the UICollectionView to determine its own size and fill UIStackView proportionally, you need to set a height constraint for the UICollectionView. And then update the constraint based on the UICollectionViewCell content height.

Your code looks good, you only need to make minor changes, Here is a solution (updated 09/25/2018):

 private func setUpConstraints() {
    contentView.addSubview(borderView)
    borderView.addSubview(actionButton)
    borderView.addSubview(seperator)
    borderView.addSubview(pageControl)
    borderView.addSubview(collectionView)

    NSLayoutConstraint.activate([
        borderView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), // ---  >. Use cell content view anchors
        borderView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), // ---  >. Use cell content view anchors
        borderView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), // ---  >. Use cell content view anchors
        borderView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), // ---  >. Use cell content view anchors
        ])


    //other constraints....

    NSLayoutConstraint.activate([
        collectionView.topAnchor.constraint(equalTo: borderView.topAnchor),
        collectionView.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
        collectionView.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
        collectionView.bottomAnchor.constraint(equalTo: pageControl.topAnchor),

    collectionView.heightAnchor.constraint(equalToConstant: 200) // ---> System needs to know height, add this constraint to collection view.
        ])
}

Also remember to set the tableView rowHeight to UITableView.automaticDimension & to give the system an estimated row height with: tableView.estimatedRowHeight.

mxlbx
  • 26
  • 3
  • First of all, thank you for the excellent explanation and great details. I agree with you and think about what you are saying is correct. But I have tried to give the vertical UIStackView in UITableViewCell with a constant height, but somehow UITableViewAutoDimension didn't assign the UITableViewCell a proper height. I will upload my code and my layout guide. Please help me to see if I am doing the it right. Thank you. – Tim Lin Sep 24 '18 at 01:32
  • Hi Tim!, I reviewed your code, actually I made an Xcode project and it worked great (only needed to create a height constraint for the collection view) – mxlbx Sep 26 '18 at 00:45
  • Hi mxlbx! Thanks for the reply. I actually know that setting a height constraint for the UICollectionView will work. But the think is that we need dynamic height for the UICollectionViewCell in this project. So I can’t really give a constant height to the UICollectionView unless I calculate the height for every cells. However, my other team members prefer not to calculate the height by ourself but leveraging Auto-layout and StackView to figure out the height by its’ content. But they are not sure if it is possible so I am stucking on this problem for days. Do you think it is possible? – Tim Lin Sep 26 '18 at 03:57
  • Have you tried setting the height with the scroll view content size (collectionView.contentLayoutGuide) or frame (collectionView.frameLayoutGuide)? Auto layout needs to know the size of UICollectionView somehow. – mxlbx Sep 26 '18 at 18:17