UICollectionViewDiffableDataSource crash: Invalid parameter not satisfying: itemCount, when fetching data both asynchronously and synchronously

Originator:rachelehyman
Number:rdar://FB8653320 Date Originated:9/8/20
Status:Open Resolved:
Product:UIKit Product Version:
Classification: Reproducible:
 
I am using a UICollectionView with UICollectionViewDiffableDataSource and fetching data both asynchronously and synchronously to populate the view. The app crashes with this message: "Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: itemCount'."

I believe it has something to do with the combination of sync and async data fetching, as when I implement one or the other the app runs as expected.

When I set up the collection view layout group like so, it crashes:
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])

A workaround is to set up the collection view layout group like this instead, explicitly providing the count:
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 1)

Single-view app view controller code to reproduce:

import UIKit

struct Model: Hashable {
  let id: UUID
}

class ViewController: UIViewController {
  
  private var collectionView: UICollectionView!
  private var dataSource: DataSource?
  private var asyncItems: [Model] = []
  private var syncItems: [Model] = []
  
  typealias DataSource = UICollectionViewDiffableDataSource<Section, Model>
  typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Model>
  
  enum Section {
    case async
    case sync
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    setUpCollectionView()
    setUpDataSource()
    
    fetchData()
  }
  
  func setUpCollectionView() {
    let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: setUpCollectionViewLayout())
    collectionView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(collectionView)
    collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
    collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
    
    collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "reuseIdentifier")
    
    self.collectionView = collectionView
  }
  
  func setUpCollectionViewLayout() -> UICollectionViewLayout {
    return UICollectionViewCompositionalLayout { (sectionIndex: Int,
      layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
      let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(1.0))
      let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100))
      
      let item = NSCollectionLayoutItem(layoutSize: itemSize)
      
      let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
      
      return NSCollectionLayoutSection(group: group)
    }
  }
  
  func setUpDataSource() {
    self.dataSource =  DataSource(collectionView: collectionView) { (collectionView, indexPath, model) -> UICollectionViewCell? in
      let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "reuseIdentifier", for: indexPath)
      return cell
    }
  }
  
  func fetchData() {
    let dispatchGroup = DispatchGroup()
    
    dispatchGroup.enter()
    DispatchQueue.global(qos: .background).async { [weak self] in
      defer {
        dispatchGroup.leave()
      }
      self?.asyncItems = [Model(id: UUID())]
    }
    
    syncItems = [Model(id: UUID())]
    
    dispatchGroup.notify(queue: .main) { [weak self] in
      self?.applySnapshot()
    }
  }
  
  func applySnapshot() {
    var snapshot = Snapshot()
    snapshot.appendSections([Section.async])
    snapshot.appendItems(asyncItems)
    
    snapshot.appendSections([Section.sync])
    snapshot.appendItems(syncItems)
    
    dataSource?.apply(snapshot)
  }
}

Comments


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!