UICollectionView with diffable data source always reload on-screen cells after applying a snapshot.

From what I concluded, a diffable data source is designed to insert/delete/update/move only the differences from the new snapshot (if animatingDifferences is true), for those irrelevent cells, they should not be reloaded.

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.



Answered by Frameworks Engineer in 677642022

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.

Updates


This behavior only happens when using a compositional layout with estimated item height or a list layout.

For compositional layouts with absolute item height, no irrelevant cell is reloaded.
Accepted Answer

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.

UICollectionView with diffable data source always reload on-screen cells after applying a snapshot.
 
 
Q