Search code examples
iosswiftuitableviewuicollectionviewcellintrinsic-content-size

iOS - How to make an UICollectionViewCell adapt its height according to its content ? (containing an UITableView)


I don't know why it is so complicated to design cells that can adapt to its content. It shouldn't need that much code, I still don't understand why UIKit can't handle this properly.

Anyway, here is my issue (I have edited the whole post):

I have an UICollectionViewCell that contains an UITableView.

Here is my sizeForItem method :

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
                        sizeForItemAt indexPath: IndexPath) -> CGSize {

        var cellWidth: CGFloat = collectionView.bounds.size.width 
        var cellHeight: CGFloat = 0

        let cellConfigurator = items[indexPath.item].cellConfigurator

        if type(of: cellConfigurator).reuseId == "MoonCollectionViewCell" {
            if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: type(of: cellConfigurator).reuseId, for: indexPath) as? MoonCollectionViewCell {
               cell.contentView.layoutIfNeeded()
               let size = cell.selfSizedTableView.intrinsicContentSize
               cellHeight = size.height
            }
        }

        return CGSize.init(width: cellWidth, height: cellHeight)
    }

sizeForItem is called before cellForItem, that's the reason of the layoutIfNeeded, because I couldn't get the correct intrinsic content size.

I have removed the XIB as suggested, and designed my UICollectionViewCell within the Storyboard.

Here is my UICollectionViewCell designed within a Storyboard (only the UITableViewCell is designed in a XIB file)

I only added an UITableView within the UICollectionViewCell.
I want the UICollectionViewCell to adapt its size according to the height of the tableView.

Now here is my tableView :

I have created a subclass of UITableView (from this post)

class SelfSizedTableView: UITableView {
    var maxHeight: CGFloat = UIScreen.main.bounds.size.height

    override func reloadData() {
        super.reloadData()
        self.invalidateIntrinsicContentSize()
        self.layoutIfNeeded()
    }

    override var intrinsicContentSize: CGSize {
        let height = min(contentSize.height, maxHeight)
        return CGSize(width: contentSize.width, height: height)
    }
}

Please note that I have disabled scrolling, I have dynamic prototype for the tableView cells, the style is grouped.

EDIT : Check the configure method, it comes from a protocol I used to configure in a generic way all my UICollectionViewCell

func configure(data: [MoonImages]) {

        selfSizedTableView.register(UINib.init(nibName: "MoonTableViewCell", bundle: nil), forCellReuseIdentifier: "MoonTableViewCell")

        selfSizedTableView.delegate = self
        selfSizedTableView.dataSource = moonDataSource

        var frame = CGRect.zero
        frame.size.height = .leastNormalMagnitude
        selfSizedTableView.tableHeaderView = UIView(frame: frame)
        selfSizedTableView.tableFooterView = UIView(frame: frame)

        selfSizedTableView.maxHeight = 240.0
        selfSizedTableView.estimatedRowHeight = 40.0
        selfSizedTableView.rowHeight = UITableView.automaticDimension

        moonDataSource.data.addAndNotify(observer: self) { [weak self] in
            self?.selfSizedTableView.reloadData()
        }

        moonDataSource.data.value = data

    }

FYI the dataSource is a custom dataSource, with dynamic value (Generics) and the observer pattern, to reload the collection/tableView when the data is set.

I also have this warning when I launch the App.

[CollectionView] An attempt to update layout information was detected while already in the process of computing the layout (i.e. reentrant call). This will result in unexpected behaviour or a crash. This may happen if a layout pass is triggered while calling out to a delegate.

Any hints or advice on how I should handle this ?

Because I am facing a strange behavior, it's like my sizeForItem use random values. The UICollectionViewCell height is not the same than my UITableView intrinsic content size height.

If I have 2 rows within my UITableView, the UICollectionView is not always equal at this size. I really don't know how to achieve this...

Should I invalideLayout?


Solution

  • Maybe it's not the answer you wanted, but here're my two cents. For your particular requirements, the better solution is moving away from UITableView, and use UIStackView or your custom container view.

    Here's why:

    1. UITableView is a subclass of UIScrollView, but since you've disabled its scrolling feature, you don't need a UIScrollView.
    2. UITableView is mainly used to reuse cells, to improve performance and make code more structured. But since you're making it as large as its content size, none of your cells are reused, so features of UITableView is not taken any advantage of.

    Thus, actually you don't need and you should not use either UITableView or UIScrollView inside the UICollectionViewCell for your requirements.

    If you agree with above part, here're some learnings from our practices:

    1. We always move most of the underlying views and code logics, mainly data assembling, into a UIView based custom view, instead of putting in UITableViewCell or UICollectionViewCell directly. Then add it to UITableViewCell or UICollectionViewCell's contentView and setup constraints. With this structure, we can reuse our custom view in more scenarios.
    2. For requirements similar to yours, we'll create a factory class to create "rows" similar to how you create "cells" for your UITableView, add them into a vertical UIStackView, create constraints deciding UIStackView's width. Auto layout will take care of the rest things.
    3. In your usage with UICollectionViewCell, to calculate the wanted height, inside preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) func of your cell, you can use contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) to calculate the height, do some check and return. Also, remember to invalidate layout when the width of the UICollectionView changes.