UICollectionViewCompositionalLayout unexpected behavior with .estimated heights

Using NSCollectionLayoutSize with .estimated dimensions in horizontal orthogonal sections, creates layout issues. The cells & supplementary views have layout conflicts, the scroll behavior is sub optimal and spacing is not as expected

Working with Xcode: 12.4 , Simulator: iOS 14.4
Layout bug:
Code Block
[LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x6000011266c0 UIView:0x7fc6c4617020.height == 80 (active)>",
"<NSLayoutConstraint:0x600001126530 V:|-(0)-[UIView:0x7fc6c4617020] (active, names: '|':UIView:0x7fc6c4616d10 )>",
"<NSLayoutConstraint:0x6000011261c0 UIView:0x7fc6c4617020.bottom == UIView:0x7fc6c4616d10.bottom (active)>",
"<NSLayoutConstraint:0x600001121360 'UIView-Encapsulated-Layout-Height' UIView:0x7fc6c4616d10.height == 50 (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x6000011266c0 UIView:0x7fc6c4617020.height == 80 (active)>

Code to reproduce:
Code Block
import UIKit
class ViewController: UIViewController {
lazy var collectionView: UICollectionView = {
let layout = createLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = self
collectionView.backgroundColor = .systemBackground
collectionView.register(Cell.self, forCellWithReuseIdentifier: "cell")
collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: "header")
return collectionView
}()
private func createLayout() -> UICollectionViewCompositionalLayout {
let sectionProvider = { (section: Int,
layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
return self.horizontalLayout(layoutEnvironment: layoutEnvironment)
}
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 8
let layout = UICollectionViewCompositionalLayout(sectionProvider: sectionProvider, configuration: config)
return layout
}
private func supplementaryHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
let titleSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50))
let titleSupplementary = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: titleSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top)
return titleSupplementary
}
private func horizontalLayout(layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection {
let size = NSCollectionLayoutSize(widthDimension: .estimated(120), heightDimension: .estimated(50))
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.interGroupSpacing = 8
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
section.boundarySupplementaryItems = [supplementaryHeader()]
return section
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
// MARK: UICollectionViewDataSource
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
return cell
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 25
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
let header: HeaderView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: "header", for: indexPath) as! HeaderView
return header
default: fatalError()
}
}
}
class Cell: UICollectionViewCell {
lazy var view: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemRed
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
fatalError("not implemented")
}
func configure() {
contentView.addSubview(view)
view.heightAnchor.constraint(equalToConstant: 80).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
view.topAnchor.constraint(equalTo: contentView.topAnchor),
view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
}
class HeaderView: UICollectionReusableView {
lazy var view: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemTeal
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
fatalError("not implemented")
}
func configure() {
addSubview(view)
view.heightAnchor.constraint(equalToConstant: 60).isActive = true
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: self.leadingAnchor),
view.trailingAnchor.constraint(equalTo: self.trailingAnchor),
view.topAnchor.constraint(equalTo: self.topAnchor),
view.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}
}



Post not yet marked as solved Up vote post of papanton Down vote post of papanton
3.6k views

Replies

I have the same problems. "estimated" NSCollectionLayoutSize dimension working bad. As I can see, it adds every estimated width/height as a constraint, which creates conflicts. ( which in my case also leads to a huge freeze).

Introduced in ios13, already ios15 coming, still not working well :(

Anyone found a solid solution for this? Calling collectionView.collectionViewLayout.invalidateLayout() immediately after my dataSource.apply() call gets the cells to display correctly prior to beginning scrolling, but it seems hacky and doesn't look great while the data loads into the cells.

I am encountering the same issue on iOS 14. My solution has been to make the estimated height greater than all of the dynamic cell's height. This however does not yield the result I am looking for.

+1 to experiencing this issue on iOS 14 and iOS 15. I have constraints set on UICollectionViewCell with estimated heights and the layout jumps during scroll because cells are different than the "estimated" height.

I have the same problems. iOS 15, but bug is still not fixed. I use compositional layout everywhere - where I don't need estimated size ;)