We encountered a deadlock scenario when our app runs on iOS 18. It happens when we use the async version of UICollectionViewDiffableDataSource.apply()
and request a section snapshot from within the UICollectionViewCompositionalLayoutSectionProvider
.
The deadlock doesn't happen when using the completion handler version of UICollectionViewDiffableDataSource.apply()
or when requesting a full snapshot from the data source.
On iOS 17 there is no issue.
import UIKit
class ViewController: UICollectionViewController {
private enum SectionIdentifier: Hashable {
case test
}
private enum ItemIdentifier: Hashable {
case test(UUID)
}
private typealias Snapshot = NSDiffableDataSourceSnapshot<SectionIdentifier, ItemIdentifier>
private typealias SectionSnapshot = NSDiffableDataSourceSectionSnapshot<ItemIdentifier>
private typealias DataSource = UICollectionViewDiffableDataSource<SectionIdentifier, ItemIdentifier>
private lazy var dataSource: DataSource = {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, UIColor> { cell, _, color in
cell.contentView.backgroundColor = color
}
return DataSource(collectionView: collectionView) { [weak self] collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: .red)
}
}()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = dataSource
collectionView.collectionViewLayout = UICollectionViewCompositionalLayout { [weak self] section, environment -> NSCollectionLayoutSection? in
// calling `UICollectionViewDiffableDataSource.snapshot(for:)` from the `UICollectionViewCompositionalLayoutSectionProvider` leads to deadlock on iOS 18
let numberOfItems = self?.dataSource.snapshot(for: .test).items.count ?? 0 // deadlock
// calling `UICollectionViewDiffableDataSource.snapshot()` causes no issue
// let numberOfItems = self?.dataSource.snapshot().numberOfItems(inSection: .test) ?? 0 // works
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(100), heightDimension: .absolute(100)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(100)), repeatingSubitem: item, count: numberOfItems)
group.interItemSpacing = .fixed(5)
return .init(group: group)
}
var snapshot = Snapshot()
snapshot.appendSections([.test])
snapshot.appendItems([.test(UUID()), .test(UUID()), .test(UUID()), .test(UUID())])
// using the async wrapper `UICollectionViewDiffableDataSource.apply()` on any thread leads to deadlock on iOS 18
Task { await dataSource.apply(snapshot) } // deadlock
// Task { @MainActor in await dataSource.apply(snapshot) } // deadlock
// using `UICollectionViewDiffableDataSource.apply()` with completion handling causes no issue
// Task { dataSource.apply(snapshot) } // works
// dataSource.apply(snapshot) // works
}
}
Full example project at https://github.com/antiraum/iOS18_UICollectionViewDiffableDataSource_deadlock
Thank you for reporting this, this is a bug in UICollectionViewDiffableDataSource
. While we work on a fix, for now, I would recommend always applying all snapshots from the main thread.
When using diffable data source, the only thing that you gain when applying a snapshot in the background is that the diff is performed off the main thread. However, for most data sources, diffing is really fast.
The slowest part of any snapshot application has always been the actual view update, where UICollectionView has to take the submitted updates and generate animations, and perform the requisite bookkeeping on its managed reusable views.
I'd encourage you to try applying all your snapshots on the main queue/thread/actor, as you will then sidestep this bug entirely.