But whenever I apply a snapshot, even if the new snapshot is exactly the current one from the data source, almost every on-screen cells got reloaded. Here's a simple test view controller:
Code Block swift import UIKit class CollectionViewController: UIViewController { enum Section: Hashable { case main } struct Item: Hashable { let id: String } private lazy var dataSource: UICollectionViewDiffableDataSource<Section, Item> = { return UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in print("data source reloading cell", item, indexPath) let cell = collectionView.dequeueReusableCell( withReuseIdentifier: "cell", for: indexPath ) as! Cell cell.item = item return cell } }() private lazy var listLayout: UICollectionViewCompositionalLayout = { let conf = UICollectionLayoutListConfiguration(appearance: .plain) return UICollectionViewCompositionalLayout.list(using: conf) }() private lazy var regularLayout: UICollectionViewCompositionalLayout = { let item = NSCollectionLayoutItem( layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50)) ) let group = NSCollectionLayoutGroup.horizontal( layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50)), subitem: item, count: 1 ) let section = NSCollectionLayoutSection(group: group) return UICollectionViewCompositionalLayout(section: section) }() private lazy var collectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: regularLayout) view.register(Cell.self, forCellWithReuseIdentifier: "cell") view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .systemBackground return view }() override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) NSLayoutConstraint.activate([ collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() snapshot.appendSections([.main]) snapshot.appendItems((0 ..< 100).map { .init(id: "\($0)") }) dataSource.apply(snapshot, animatingDifferences: false) Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { _ in print("\n\nDelayed Reloading") var snapshot = self.dataSource.snapshot() self.dataSource.apply(snapshot, animatingDifferences: true) } } class Cell: UICollectionViewCell { var item: Item? { didSet { label.text = item?.id } } private(set) lazy var label: UILabel = { let view = UILabel() view.font = .systemFont(ofSize: 48, weight: .semibold) view.translatesAutoresizingMaskIntoConstraints = false return view }() override init(frame: CGRect) { super.init(frame: frame) print("cell inited") contentView.addSubview(label) NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12), ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } }
Output
Xcode 12.5
iPhone 11 device (iOS 14.5.1)
Code Block Delayed Reloading data source reloading cell Item(id: "2") [0, 2] data source reloading cell Item(id: "4") [0, 4] data source reloading cell Item(id: "6") [0, 6] data source reloading cell Item(id: "8") [0, 8] data source reloading cell Item(id: "10") [0, 10] data source reloading cell Item(id: "12") [0, 12] data source reloading cell Item(id: "14") [0, 14] data source reloading cell Item(id: "1") [0, 1] data source reloading cell Item(id: "3") [0, 3] data source reloading cell Item(id: "5") [0, 5] data source reloading cell Item(id: "7") [0, 7] data source reloading cell Item(id: "9") [0, 9] data source reloading cell Item(id: "11") [0, 11] data source reloading cell Item(id: "13") [0, 13] data source reloading cell Item(id: "15") [0, 15] data source reloading cell Item(id: "0") [0, 0] data source reloading cell Item(id: "11") [0, 11]
Is this an expected behavior?
I've also run the same test on UITableView, which is much more reasonable. Only some off-screen cells are reloaded.
In iOS 14, when using a layout with estimated sizes, UICollectionView will request additional cells during certain operations to perform self-sizing (also known as updating preferred sizes). Those cells may not actually become visible afterwards; in many cases the cells are immediately put back into the reuse pool once they have been self-sized. So just because you see cells being requested doesn't mean that the existing visible cells are being updated or replaced.
In iOS 15, UICollectionView will avoid requesting extra cells just for self-sizing in most cases — instead, it will use existing cells to perform self-sizing whenever possible. Therefore, you should see far fewer (if any) extra cells being requested in these cases on iOS 15.