Reputation: 5103
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)"
}
}
Upvotes: 6
Views: 3594
Reputation: 534885
I think you've put your finger on it. When you say animatingDifferences
is to be false
, you are asking the diffable data source to behave as if it were not a diffable data source. You are saying: "Skip all that diffable stuff and just accept this new data." In other words, you are saying the equivalent of reloadData()
. No new cells are created (it's easy to prove that by logging), because all the cells are already visible; but by the same token, all the visible cells are reconfigured, which is exactly what one expects from saying reloadData()
.
When animatingDifferences
is true
, on the other hand, the diffable data source thinks hard about what has changed, so that, if necessary, it can animate it. As a result of all that work behind the scenes, therefore, it knows when it can avoid reloading a cell if it doesn't have to (because it can move the cell instead).
Indeed, when animatingDifferences
is true
, you can apply a snapshot that reverses the cells, and yet configure
is never called again, because moving the cells around is all that needs to be done:
func updateSnapshot(animatingChange: Bool = true) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.all])
self.items = self.items.reversed()
snapshot.appendItems(self.items, toSection: .all)
self.dataSource.apply(snapshot, animatingDifferences: animatingChange)
}
Interestingly, I also tried the above with shuffled
instead of reversed
, and I found that sometimes some cells are reconfigured. Evidently it is not the main intention of the diffable data source not to reload cells; it's just a sort of side effect.
Upvotes: 9