I've created the following demo view controller to reproduce the issue in a minimal example.
Here I'm applying a snapshot of the same data repeatedly to the same collection view using UICollectionViewDiffableDataSource and every time all of the cells are reloaded even though nothing has changed.
I'm wondering if this is a bug, or if I'm "holding it wrong".
It looks like this other user had the same issue, though they didn't provide enough information to reproduce the bug exactly: iOS UICollectionViewDiffableDataSource reloads all data with no changes
EDIT: I've also uncovered a strange behavior - if animating differences is true
, the cells are not reloaded every time.
import UIKit
enum Section {
case all
}
struct Item: Hashable {
var name: String = ""
var price: Double = 0.0
init(name: String, price: Double) {
self.name = name
self.price = price
}
}
class ViewController: UIViewController {
private let reuseIdentifier = "ItemCell"
private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
private lazy var dataSource = self.configureDataSource()
private var items: [Item] = [
Item(name: "candle", price: 3.99),
Item(name: "cat", price: 2.99),
Item(name: "dribbble", price: 1.99),
Item(name: "ghost", price: 4.99),
Item(name: "hat", price: 2.99),
Item(name: "owl", price: 5.99),
Item(name: "pot", price: 1.99),
Item(name: "pumkin", price: 0.99),
Item(name: "rip", price: 7.99),
Item(name: "skull", price: 8.99),
Item(name: "sky", price: 0.99),
Item(name: "book", price: 2.99)
]
override func viewDidLoad() {
super.viewDidLoad()
// Configure the collection view:
self.collectionView.backgroundColor = .white
self.collectionView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.register(ItemCollectionViewCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
self.collectionView.dataSource = self.dataSource
self.view.addSubview(self.collectionView)
NSLayoutConstraint.activate([
self.collectionView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.collectionView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.collectionView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
])
// Configure the layout:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1/3))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
self.collectionView.setCollectionViewLayout(layout, animated: false)
// Update the snapshot:
self.updateSnapshot()
// Update the snapshot once a second:
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateSnapshot()
}
}
func configureDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: self.collectionView) { (collectionView, indexPath, item) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier, for: indexPath) as! ItemCollectionViewCell
cell.configure(for: item)
return cell
}
return dataSource
}
func updateSnapshot(animatingChange: Bool = false) {
// Create a snapshot and populate the data
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.all])
snapshot.appendItems(self.items, toSection: .all)
self.dataSource.apply(snapshot, animatingDifferences: false)
}
}
class ItemCollectionViewCell: UICollectionViewCell {
private let nameLabel = UILabel()
private let priceLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.nameLabel)
self.addSubview(self.priceLabel)
self.translatesAutoresizingMaskIntoConstraints = false
self.nameLabel.translatesAutoresizingMaskIntoConstraints = false
self.nameLabel.textAlignment = .center
self.priceLabel.translatesAutoresizingMaskIntoConstraints = false
self.priceLabel.textAlignment = .center
NSLayoutConstraint.activate([
self.nameLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.nameLabel.topAnchor.constraint(equalTo: self.topAnchor),
self.nameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor),
self.priceLabel.topAnchor.constraint(equalTo: self.nameLabel.bottomAnchor),
self.priceLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.priceLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(for item: Item) {
print("Configuring cell for item \(item)")
self.nameLabel.text = item.name
self.priceLabel.text = "$\(item.price)"
}
}