Search code examples
swiftuicollectionviewcelluicollectionviewlayoutuicollectionviewcompositionallayout

Compositional Layout item with .estimated width crashes app when content is too long


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
    }
}

Solution

  • 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
                ...
            }
        }