alfogrillo
alfogrillo

Reputation: 519

UICollectionViewCompositionalLayout bug on iOS 14.3

I'm experiencing a weird layout issue on iOS 14.3 with collection views using a UICollectionViewCompositionalLayout combined in my case with the UICollectionViewDiffableDataSource. The issue is about the wrong position of the inner _UICollectionViewOrthogonalScrollerEmbeddedScrollView when you have an orthogonal section preceded by an intrinsic height section.

Fortunately I'm able to reproduce the issue very easily. Consider having this data source:

private var dataSource: UICollectionViewDiffableDataSource<Section, String>!

enum Section: Int, Hashable, CaseIterable {
    case first = 0
    case second = 1
}

For each section you create the following layout:

private extension Section {
    var section: NSCollectionLayoutSection {
        switch self {
        case .first:
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
            let section: NSCollectionLayoutSection = .init(group: group)
            return section
        case .second:
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(200), heightDimension: .absolute(200))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
            let section: NSCollectionLayoutSection = .init(group: group)
            section.orthogonalScrollingBehavior = .continuous
            section.contentInsets = .init(top: 10, leading: 10, bottom: 10, trailing: 10)
            section.interGroupSpacing = 10
            return section
        }
    }
}

The thing breaking the layout is having into the .first section both itemSize and groupSize with .estimated height.

You can see the result below on iOS 14.3: at the first glance the layout is visually correct, but you immediately realize the fact that it's broken because the inner scroll view is in the wrong place. This implies that the horizontal scroll happens wrongly in the blue area.

Broken layout iOS 14.3

Running exactly the same code up to iOS 14.2 you get the correct layout . Correct Layout

What do you think about this issue? Am I missing something or it could be a UIKit bug?

Thanks

Upvotes: 18

Views: 5594

Answers (4)

heyfrank
heyfrank

Reputation: 5647

I got the same issue and the workarounds of the other answers didn't work for me.

So I made this extension to determine if it's the buggy iOS-Version:

extension UICollectionView {
    static var isIosVersionWithSizeEstimationBug: Bool {
        if #available(iOS 14.5, *) {
            return false
        }
        if #available(iOS 14.3, *) {
            return true
        }
        return false
    }
}

And I use this to use an absolute height in this case. It might not be a workaround for every use case. But for me it's a stable solution:

let height: NSCollectionLayoutDimension = {
    let maxPossibleHeight: CGFloat = 280
    if UICollectionView.isIosVersionWithSizeEstimationBug {
        return .absolute(maxPossibleHeight)
    } else {
        return .estimated(maxPossibleHeight)
    }
}()
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: height)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: height)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)

Upvotes: 2

craigcoded
craigcoded

Reputation: 61

We have a slightly different use case where the section with the scroll view also uses estimated heights, so I modified softenhard's solution to also adjust the height of the scrollview.

public final class CollectionView: UICollectionView {
    override public func layoutSubviews() {
        super.layoutSubviews()

        guard #available(iOS 14.3, *) else { return }

        subviews.forEach { subview in
            guard
                let scrollView = subview as? UIScrollView,
                let minY = scrollView.subviews.map(\.frame.origin.y).min(),
                let maxHeight = scrollView.subviews.map(\.frame.height).max(),
                minY > scrollView.frame.minY || maxHeight > scrollView.frame.height
            else { return }

            scrollView.contentInset.top = -minY
            scrollView.frame.origin.y = minY
            scrollView.frame.size.height = maxHeight
        }
    }
}

Upvotes: 6

Marwen Doukh
Marwen Doukh

Reputation: 2050

For me this problem is reproducible in iOS 14.3 and 14.4 . And now is fixed in iOS 14.5 beta1

Try to install the latest Xcode beta version 12.5 beta and test it using a simulator running iOS 14.5

Xcode beta link: https://developer.apple.com/download/

Upvotes: 6

sftnhrd
sftnhrd

Reputation: 140

We have header with estimated height and some sections with _UICollectionViewOrthogonalScrollerEmbeddedScrollView were completely broken because of this regression. So here is a solution that worked in our case

public final class CollectionView: UICollectionView {
    
    public override func layoutSubviews() {
        super.layoutSubviews()
    
        guard #available(iOS 14.3, *) else { return }
    
        subviews.forEach { subview in
            guard
                let scrollView = subview as? UIScrollView,
                let minY = scrollView.subviews.map(\.frame.origin.y).min(),
                minY > scrollView.frame.minY
            else { return }
    
            scrollView.contentInset.top = -minY
            scrollView.frame.origin.y = minY
        }
    }
}

Upvotes: 12

Related Questions