Self-sizing cells with UICollectionViewCompositionalLayout
Asked Answered
C

1

0

I have a view controller that displays a collection view with self-sizing cells. The collection view has one section that scrolls horizontally. It looks like this:

collection view

Problem

The collection view behaves unexpectedly when the view controller is presented using the default pageSheet style on iOS 13+.

  1. When pulling upward on the sheet, cells may appear to resize like the cell labeled "Rectify" below:

bug 1

  1. When pulling upward on the sheet, the content may shift horizontally. Sometimes, cells may disappear too:

bug 2

Question

Is there a way to fix this behavior while still using UICollectionViewCompositionalLayout and the pageSheet presentation style?

Code Summary

The code is pretty straightforward. Just 3 classes, which can be dropped into the ViewController.swift file using the Single View App project template in Xcode.

  1. A UICollectionViewCell class called Cell. The cell has a UILabel and overrides sizeThatFits(_:).
  2. A UIViewController called ViewController used only to present BugViewController.
  3. BugViewController, which configures the data source and presents the collection view. This is where the problem occurs.

Code

import UIKit

// MARK: - Cell -

final class Cell: UICollectionViewCell {
    static let reuseIdentifier = "Cell"

    lazy var label: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        label.frame.size = contentView.bounds.size
        label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.addSubview(label)
        contentView.backgroundColor = .tertiarySystemFill
    }

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

    override func sizeThatFits(_ size: CGSize) -> CGSize {
        .init(width: label.sizeThatFits(size).width + 32, height: 32)
    }
}

// MARK: - ViewController -

final class ViewController: UIViewController {
    private let button: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Tap Me!".uppercased(), for: .normal)
        button.addTarget(self, action: #selector(presentBugViewController), for: .touchUpInside)
        button.sizeToFit()
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(button)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        button.center = view.center
    }

    @objc func presentBugViewController() {
        present(BugViewController(), animated: true)
    }
}

// MARK: - BugViewController -

final class BugViewController: UIViewController {
    private let models = [
        "Better Call Saul",
        "Mad Men",
        "Rectify",
        "Tiger King: Murder, Mayhem, and Madness",
        "Master of None",
        "BoJack Horseman"
    ]

    private lazy var collectionView: UICollectionView = {
        let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout())
        collectionView.register(Cell.self, forCellWithReuseIdentifier: Cell.reuseIdentifier)
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.contentInset.top = 44
        collectionView.backgroundColor = .white
        return collectionView
    }()

    private lazy var dataSource = UICollectionViewDiffableDataSource<Int, String>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else { fatalError() }
        cell.label.text = itemIdentifier
        return cell
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(collectionView)

        var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
        snapshot.appendSections([0])
        snapshot.appendItems(models)
        dataSource.apply(snapshot)
    }

    private func createCollectionViewLayout() -> UICollectionViewCompositionalLayout {
        let layoutSize = NSCollectionLayoutSize.init(
            widthDimension: .estimated(200),
            heightDimension: .absolute(32)
        )

        let section = NSCollectionLayoutSection(group:
            .horizontal(
                layoutSize: layoutSize,
                subitems: [.init(layoutSize: layoutSize)]
            )
        )
        section.interGroupSpacing = 8
        section.orthogonalScrollingBehavior = .continuous

        return .init(section: section)
    }
}

Notes

  • The collection view in my app actually has many sections and scrolls vertically. That is why I'm using a vertically scrolling collection view and a section with orthogonalScrollingBehavior in the example code.

Failed Attempts

  • I've tried using Auto Layout constraints instead of sizeThatFits(_:).
  • I've tried not using UICollectionViewDiffableDataSource.

Workarounds

Modifying the cell with a child scroll view and passing in an array of strings (as opposed to one at a time) does avoid this problem. But, it's a dirty hack that I'd like to avoid if possible.

Candlefish answered 29/4, 2020 at 10:3 Comment(4)
If you're using compositional layout, why are you using a cell with sizeThatFits? Those are opposed techniques. You should be using autolayout, not autoresizing. — Also if there is only one section, why is this a vertical scrolling collection view with an orthogonal scrolling section? Why isn't this just a horizontal scrolling collection view?Heterotypic
@Heterotypic Thanks for responding! Admittedly, I’m confused by your first question. Opposing techniques? To your second question, the collection view in my app scrolls vertically with multiple sections. I’ll make an edit to clarify.Candlefish
Fwiw, I did try using Auto Layout constraints instead of sizeThatFits. Same issues.Candlefish
OK fair enough, just trying to eliminate sources of error. Using autoresizing plus size that fits is very weird stuff in this day and age. :)Heterotypic
U
0

i find a solution, just use xib , no need to modify your constraints, u just find the cell resizing work well with the compositionallayout, i do not know why, it just work

Untune answered 11/9, 2024 at 4:5 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.