I'm exploring the amazing world of using Compositional Layout and I've found a small situation that I need help with. Below there is a simple app that uses a CollectionView with CL to display random strings using a UIListContentConfiguration
rounded cell. The item's width is .estimated(30)
so I can get self-sizing cell (width) based on the content of the cell. This works perfectly until I increase the number of characters from 50 to , let's say, 100. (currently running on an iPad Pro 9.7). Seems like 100 characters exceeds the width of the CollectionView making my app start using crazy amount of memory until it crashes because of that same reason.
How to reproduce:
Change the number of characters to be display to a higher number. Ex.
return (1...100).compactMap { $0; return self.randomString(length: .random(in: 1..<150))}
import UIKit
class ViewController: UICollectionViewController {
private let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
private lazy var values : [String] = {
return (1...100).compactMap { $0; return self.randomString(length: .random(in: 1..<50))}
}()
private func randomString(length: Int) -> String {
return String((0..<length).map{ _ in letters.randomElement()! })
}
var compositionalLayout : UICollectionViewCompositionalLayout = {
let inset: CGFloat = 2
//Item
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(30), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// Group
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.interItemSpacing = .fixed(4)
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(4), top: .fixed(4), trailing: .fixed(0), bottom: .fixed(0))
// Section
let section = NSCollectionLayoutSection(group: group)
return UICollectionViewCompositionalLayout(section: section)
}()
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
private func configureUI() {
self.collectionView.collectionViewLayout = compositionalLayout
self.collectionView.dataSource = self
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
}
}
extension ViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return values.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var contentConfiguration = UIListContentConfiguration.valueCell()
contentConfiguration.text = values[indexPath.item]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.contentConfiguration = contentConfiguration
cell.backgroundColor = UIColor(red: .random(in: 0...1) , green: .random(in: 0...1), blue: .random(in: 0...1), alpha: 1)
cell.layer.cornerRadius = cell.frame.height / 2
return cell
}
}
This appears to be a bug in Apple's compositional layout. It is easy to reproduce as you have shown in your question. When the width of the cell exceeds the collection view's width, the app hangs because it is doing excessive work on the main thread. At this point (when the app freezes or becomes unresponsive) you can stop the execution via XCode and see that Apple's internal API is going into some sort of an infinite loop while constantly allocating memory. Hence the eventual crash...
Unfortunately, the solution/work-around is to limit the width of the cells to a reasonable amount so that they won't exceed the collection view's own width. Perhaps using auto-layout rules or perhaps using the layout information you obtain from UICollectionViewCompositionalLayout
such as this:
func createCompositionalLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { (section, layoutEnvironment) -> NSCollectionLayoutSection? in
// use availableWidth variable to determine the max size for your cells
let availableWidth: CGFloat = layoutEnvironment.container.effectiveContentSize.width
...
... do some additional layout work
...
}
}