Search code examples
iosswiftuicollectionviewuicollectionviewlayoutuicollectionviewcompositionallayout

Nested UICollectionView with UICollectionViewCompositionalLayout


I have two UICollectionViews, UICV1 which has UICV2 nested within one of its cells. What I want is that UICV2 self-sizes according to its content, but both the contentSize and collectionViewLayout.collectionViewContentSize is zero.

This exact same setup worked before, the only difference is that UICV2 was a UITableView. Due to code refactoring and reuse, I switched to using UICollectionView. The new view hierarchy looks something like that:

+--------------------+
| UICV1              |
| +----------------+ |
| | Cell           | |
| | +------------+ | |
| | | Stack View | | |
| | | +--------+ | | |
| | | | UICV2  | | | |
| | | +--------+ | | |
| | +------------+ | |
| +----------------+ |
| ...                |
+--------------------+

Both collection views are configured with a UICollectionViewCompositionalLayout:

// UICV1
let layout1 = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
    let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .estimated(100)
    )

    let item = NSCollectionLayoutItem(layoutSize: itemSize)

    let group = NSCollectionLayoutGroup.horizontal(
        layoutSize: itemSize,
        subitem: item,
        count: 1
    )

    return NSCollectionLayoutSection(group: group)
}

// UICV2
let layout2 = UICollectionViewCompositionalLayout.list(using: {
    var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
    configuration.headerMode = .none
    configuration.headerTopPadding = 0
    return configuration
}())

let collectionView = SelfSizingCollectionView(
    frame: .zero,
    collectionViewLayout: layout2
)

This is the code for the self sizing collection view:

class SelfSizingCollectionView: UICollectionView {
    
    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        isScrollEnabled = false
        contentInsetAdjustmentBehavior = .never
    }
        
    override var contentSize: CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        invalidateIntrinsicContentSize()
    }

    override var intrinsicContentSize: CGSize {
        layoutIfNeeded()
        
        // This is always (0.0, 0.0)
        return CGSize(
            width: UIView.noIntrinsicMetric,
            height: collectionViewLayout.collectionViewContentSize.height
        )
    }

}

I am using a NSDiffableDataSourceSnapshot for providing data to both collection views, if this makes any difference.

I hope someone could help me here, I'm totally lost after playing with combinations of invalidateLayout(), setNeedsLayout() and layoutIfNeeded() at various points within SelfSizingCollectionView.

I found a ton of answers regarding self-sizing collection views, which I incorporated into my code, but they don't seem to work for the quite new UICollectionViewCompositionalLayout.


Solution

  • I've fixed it for now by passing the initial bounds to the SelfSizingCollectionView. Don't know if this comes with caveats or any unforeseen side effects but works for now.

    let collectionView = SelfSizingCollectionView(
        frame: bounds,
        collectionViewLayout: layout2
    )