Reputation: 519
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.
Running exactly the same code up to iOS 14.2 you get the 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
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
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
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
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