What happened/changed with UICollectionViewCompositionalLayout and dynamic/auto-sizing cells and headers/footers?

Since UICollectionViewCompositionalLayout released, I've been able to create dynamic-height UICollectionViewCells without issue using AutoLayout. However, I noticed today that it seems this no longer works.

I have tried all combinations of setNeedsLayout, layoutIfNeeded, changing the autoresizing mask on the cell, header, collectionView, labels... nothing changes. However, it DOES correctly size the header/cell once they are reused. If you run the example code below, scroll down until they are off-screen, and they will be correctly re-sized when they reappear (which is how they used to appear on first load).

So what is wrong with the below code? Previously, this would correctly size the header and cell to fit the content (in this case, a multi-line label). Is there something obvious I'm missing? The code below is the same format I've been using for over a year without issue until now. All suggestions/advice welcome!

  • macOS Ventura 13.3.1
  • Xcode 14.3.1
  • Running on iOS 16.4 simulator
  • Issue also present in iOS 16.0 simulator
  • Issue also present on physical device (iPhone 14 Pro Max, iOS 16.5.1)

ViewController:

class ViewController: UIViewController {
    
    enum Section: Int, CaseIterable { case main }
    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, AnyHashable>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViewController()
        setupCollectionView()
        setupDataSource()
        updateData()
    }
    
    private func updateData() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>()
        snapshot.appendSections(Section.allCases)
        snapshot.appendItems(Array(0..<1), toSection: .main)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
    
    private func setupViewController() {
        navigationItem.title = "Title"
        view.backgroundColor = .systemGroupedBackground
    }
}

extension ViewController: UICollectionViewDelegate {
    
    private func setupCollectionView() {
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: makeLayout())
        collectionView.backgroundColor = .clear
        collectionView.delegate = self
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(collectionView)
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    private func setupDataSource() {
        let header = UICollectionView.SupplementaryRegistration<CustomHeader>(elementKind: UICollectionView.elementKindSectionHeader) { header, kind, indexPath in
            header.set(headerText: "A really really long header title that is so long that it cannot fit on a single line, so it must span multiple lines.")
        }
        let cell = UICollectionView.CellRegistration<CustomCell, AnyHashable> { cell, indexPath, item in
            cell.set(cellText: "A really really long chunk of text that is so long that it cannot fit on a single line, so it must span multiple lines.")
        }
        
        dataSource = UICollectionViewDiffableDataSource<Section, AnyHashable>(collectionView: collectionView) { collectionView, indexPath, item in
            return collectionView.dequeueConfiguredReusableCell(using: cell, for: indexPath, item: item)
        }
        dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
            return collectionView.dequeueConfiguredReusableSupplementary(using: header, for: indexPath)
        }
    }
    
    private func makeLayout() -> UICollectionViewLayout {
        return UICollectionViewCompositionalLayout { sectionIndex, environment in
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
            
            let section = NSCollectionLayoutSection(group: group)
            section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20)
            
            let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50))
            let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
            section.boundarySupplementaryItems = [header]
            return section
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        collectionView.deselectItem(at: indexPath, animated: true)
    }
}

Screenshot

Here are the CustomHeader and CustomCell classes (restricted due to post character limit):

CustomHeader:

class CustomHeader: UICollectionReusableView {
    
    private lazy var label = makeLabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.systemBlue.withAlphaComponent(0.10)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func set(headerText: String) {
        DispatchQueue.main.async {
            self.label.text = headerText
        }
    }
    
    private func setupViews() {
        addSubview(label)
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: topAnchor),
            label.leadingAnchor.constraint(equalTo: leadingAnchor),
            label.trailingAnchor.constraint(equalTo: trailingAnchor),
            label.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
    
    private func makeLabel() -> UILabel {
        let label = UILabel()
        label.text = "Header Title"
        label.font = UIFont.systemFont(ofSize: 18, weight: .semibold)
        label.textColor = .label
        label.textAlignment = .left
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }
}

CustomCell:

class CustomCell: UICollectionViewCell {
    
    private lazy var label = makeLabel()
    private let padding: CGFloat = 20
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func set(cellText: String) {
        DispatchQueue.main.async {
            self.label.text = cellText
        }
    }
    
    private func setupViews() {
        backgroundColor = .secondarySystemGroupedBackground
        layer.cornerRadius = 12
        layer.cornerCurve = .continuous
        
        contentView.addSubview(label)
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
            label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
            label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
            label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding)
        ])
    }
    
    private func makeLabel() -> UILabel {
        let label = UILabel()
        label.text = "No text available."
        label.font = UIFont.systemFont(ofSize: 15, weight: .regular)
        label.textColor = .label
        label.textAlignment = .left
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }
}

Screen shot of what it's supposed to look like on first load, but instead currently it only looks like this after the supplementary view/cell have been reused:

What happened/changed with UICollectionViewCompositionalLayout and dynamic/auto-sizing cells and headers/footers?
 
 
Q