UICollectionViewDiffableDataSource deadlock scenario on iOS 18

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

Answered by Apple Staff in 792235022

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.

Accepted Answer

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.

Thank you for confirming the bug.

The feedback ID is FB13942310

This was fixed in iOS 18 beta4.

UICollectionViewDiffableDataSource deadlock scenario on iOS 18
 
 
Q