4

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
    }
}
rudymatos
  • 321
  • 1
  • 3
  • 12
  • 1
    It seems to me that the problem is not so much the user of estimated size as the use of UIListContentConfiguration. Because you're using this, you have no way to limit the width to which the label can grow. – matt Apr 09 '21 at 23:56
  • Well, with your comment you gave me a couple of ideas including that I can limit the label's width constraint to be less a couple of points than the collection.frame.width if the label intrinsicContentSize.width is larger than the collectionView.frame.width and that did it!. Thanks @matt – rudymatos Apr 12 '21 at 14:05
  • Yes, that is obvious, but I don't see how you can do it while confining yourself to UIListContentConfiguration, as you have no direct access to the label from that direction. That's my point. – matt Apr 12 '21 at 20:17
  • Any solution you got @rudymatos? – iOS Lifee Mar 11 '22 at 12:07
  • @iOSLifee please look at the comment above – rudymatos May 04 '22 at 17:46

1 Answers1

4

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
            ...
        }
    }
deniz
  • 725
  • 10
  • 13