18

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

alfogrillo
  • 519
  • 5
  • 15
  • Any change in behavior from iOS 14.2 to 14.3 is worth reporting to Apple. – matt Dec 19 '20 at 17:31
  • 2
    Hi matt, we reported the issue to Apple a week ago, let's wait for an answer. I'll keep the topic updated when I'll get any update. Thank you for your answer. – alfogrillo Dec 23 '20 at 08:30
  • 3
    We are running into the exact same issue. It seems to happen when `NSCollectionLayoutDimension`'s `estimatedHeight` is set to a value that is higher than the actual cell that is being rendered. When you set the `estimatedHeight` to the smallest size, it works as expected so upscaling seems to work but downscaling seems to cause the `_UICollectionViewOrthogonalScrollerEmbeddedScrollView` to get mispositioned. We're currently working around the issue by wrapping the `estimatedHeight` between `if #available(iOS 14.3, *) { ... } else { ... }`. Working on filing a `RADAR` as well. – Jeroen Dec 23 '20 at 08:41
  • Thanks for keeping up with this. – matt Dec 23 '20 at 19:58
  • @drakon any update from Apple? – NSDavidObject Jan 07 '21 at 07:20
  • 1
    Hi everyone, the Developer Technical Support answered us that the collection view team is aware of the bug and should release a fix in the next iOS versions. Sadly, they didn't provide us an estimated release date for the fix. – alfogrillo Jan 11 '21 at 15:16
  • 2
    Did a RADAR get filed for this? I'd like to monitor it if so please – nodediggity Jan 20 '21 at 11:52
  • 2
    I did file an issue with Apple, but it's still marked as Open and has not been groomed yet. I just checked Xcode 12.4 RC / iOS 14.4 RC and the issue is unfortunately still present. – Jeroen Jan 22 '21 at 14:56
  • 1
    I noticed the bug is still there on the official iOS 14.4 release. Still waiting for an answer by Apple... – alfogrillo Feb 02 '21 at 09:08

4 Answers4

12

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
        }
    }
}
sftnhrd
  • 140
  • 6
6

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
        }
    }
}
heyfrank
  • 5,291
  • 3
  • 32
  • 46
craigcoded
  • 61
  • 2
  • This one worked better for us too, originally looked like the previous one worked but due to the intermittent nature of the problem, it sometimes resulted in a shifted view. – possen Mar 10 '21 at 04:18
  • thought that it fixed it better but we are still having issue, intermittently. – possen Mar 15 '21 at 21:46
  • Please update the code to use ```guard #available(iOS 14.3, *) else { return }``` instead of this code because its wrong ```if #available(iOS 14.3, *) { return }``` – Ehab Saifan Mar 16 '21 at 15:15
  • Thanks, @EhabSaifan. Updated. – craigcoded Mar 16 '21 at 22:23
6

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/

Marwen Doukh
  • 1,946
  • 17
  • 26
2

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)
heyfrank
  • 5,291
  • 3
  • 32
  • 46