Sergio Bost
Sergio Bost

Reputation: 3239

iOS 15 - UICollectionView crash - Attempted to dequeue a cell using a registration that was created inside -collectionView:cellForItemAtIndexPath:

Here is the error:


Thread 1: "Attempted to dequeue a cell using a registration that was created inside -collectionView:cellForItemAtIndexPath: or inside a UICollectionViewDiffableDataSource cell provider. Creating a new registration each time a cell is requested will prevent reuse and cause created cells to remain inaccessible in memory for the lifetime of the collection view. Registrations should be created up front and reused. Registration: <UICollectionViewCellRegistration: 0x60000135b300>"


Here is file that is causing the error:

import UIKit

class PetExplorerViewController: UICollectionViewController {
  // MARK: - Properties
  var adoptions = Set<Pet>()
  
  lazy var dataSource = makeDataSource()

  // MARK: - Types
  enum Section: Int, CaseIterable, Hashable {
    case availablePets
    case adoptedPets
  }
  typealias DataSource = UICollectionViewDiffableDataSource<Section, Item>

  // MARK: - Life Cycle
  override func viewDidLoad() {
    super.viewDidLoad()
    navigationItem.title = "Pet Explorer"
    configureLayout()
    applyInitialSnapshots()
  }

  // MARK: - Functions
  
  func configureLayout() {
    let configuration = UICollectionLayoutListConfiguration(appearance: .grouped)
    collectionView.collectionViewLayout = UICollectionViewCompositionalLayout.list(using: configuration)
  }
  
  func applyInitialSnapshots() {
    var categorySnapshots = NSDiffableDataSourceSnapshot<Section, Item>()
    
    let categories = Pet.Category.allCases.map { category in
      return Item(title: String(describing: category))
    }
    
    categorySnapshots.appendSections([.availablePets])
    categorySnapshots.appendItems(categories, toSection: .availablePets)
    dataSource.apply(categorySnapshots, animatingDifferences: false)
  }
  
  func makeDataSource() -> DataSource {
    return DataSource(collectionView: collectionView) {collectionView, indexPath, item in
      return collectionView.dequeueConfiguredReusableCell(using: self.categoryCellRegistration(), for: indexPath, item: item)
    }
  }

  
}

// MARK: - CollectionView Cells
extension PetExplorerViewController {
}

// MARK: - UICollectionViewDelegate
extension PetExplorerViewController {
  override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  }

  func pushDetailForPet(_ pet: Pet, withAdoptionStatus isAdopted: Bool) {
    let storyboard = UIStoryboard(name: "Main", bundle: .main)
    let petDetailViewController =
      storyboard.instantiateViewController(identifier: "PetDetailViewController") { coder in
        return PetDetailViewController(coder: coder, pet: pet)
      }
    petDetailViewController.delegate = self
    petDetailViewController.isAdopted = isAdopted
    navigationController?.pushViewController(petDetailViewController, animated: true)
  }
}

// MARK: - PetDetailViewControllerDelegate
extension PetExplorerViewController: PetDetailViewControllerDelegate {
  func petDetailViewController(_ petDetailViewController: PetDetailViewController, didAdoptPet pet: Pet) {
    
  }
  
  func categoryCellRegistration() -> UICollectionView.CellRegistration<UICollectionViewListCell, Item> {
    
    return .init  { cell, _, item in
      var configuration = cell.defaultContentConfiguration()
      configuration.text = item.title
      cell.contentConfiguration = configuration
    }
  }
  
}

I think that should be all that's needed, but would be more than willing to add any other relevant needed for troubleshooting.

Upvotes: 5

Views: 3483

Answers (3)

Rostislav Balanyuk
Rostislav Balanyuk

Reputation: 1

It can also happen if you store your registration as static let somewhere but call it somewhere but not in UICollectionViewDiffableDataSource.CellProvider closure

to fix it you can just add all registrations in capture list

final class DiffableDataSource<Section: SectionProtocol, Item: ItemProtocol>:
UICollectionViewDiffableDataSource<Section, Item> {

/// diffable dataSources owns a view, if you wand different dataSource
/// make a different collectionView
required init(collectionView: UICollectionView) {

    let registrations = [
        TestingTextCell.registration,
        TestingColorCell.registration,
        TestingSuplementaryFooterView.registration,
        TestingSuplementaryHeaderView.registration,
        TestingSuplementaryDividerView.registration
    ] as [Any]

    let cellProvider: CellProvider = { [registrations] collectionView, indexPath, item in
        item.dequeueConfiguredReusableCell(in: collectionView, for: indexPath)
    }

    super.init(collectionView: collectionView, cellProvider: cellProvider)
    self.supplementaryViewProvider = suplementaryProvider
}

supplementaryViewProvider doesn't require capture list if cellProvider already captured it

Upvotes: 0

Tarun Tyagi
Tarun Tyagi

Reputation: 10112

This was added in iOS 15 for warning you against API misuse. There's an explanation for it here -

UICollectionView raises an exception when dequeuing a cell using a registration on iOS 15

Creating registrations inside the cell provider isn't supported on iOS 15, even if you were being extra careful to only create each type of registration exactly once (e.g. by storing them in lazy properties). But the fix is easy: just make sure that your registrations are created before any cells are requested.

Here's what you should do -

// Remove following -
/*
func categoryCellRegistration() -> UICollectionView.CellRegistration<UICollectionViewListCell, Item> {
    
    return .init  { cell, _, item in
      var configuration = cell.defaultContentConfiguration()
      configuration.text = item.title
      cell.contentConfiguration = configuration
    }
  }
*/

// Add following
let categoryCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { cell, _, item in 
    var configuration = cell.defaultContentConfiguration()
    configuration.text = item.title
    cell.contentConfiguration = configuration
}

// Update this line -
// `categoryCellRegistration` is not a function anymore
return collectionView.dequeueConfiguredReusableCell(using: self.categoryCellRegistration, for: indexPath, item: item)

Upvotes: 8

matt
matt

Reputation: 535889

The problem is your categoryCellRegistration. This should be a constant property (a UICollectionView.CellRegistration instance), not a method that you call every time.

Upvotes: 2

Related Questions