为什么 UICollectionViewDiffableDataSource 在没有任何变化的情况下重新加载每个单元格?
Why is UICollectionViewDiffableDataSource reloading every cell when nothing has changed?
我创建了以下演示视图控制器以在最小示例中重现该问题。
在这里,我使用 UICollectionViewDiffableDataSource 将相同数据的快照重复应用到相同的集合视图,并且每次重新加载所有单元格时,即使没有任何更改。
我想知道这是一个错误,还是我“持有错误”。
其他用户似乎遇到了同样的问题,但他们没有提供足够的信息来准确重现错误:
iOS UICollectionViewDiffableDataSource reloads all data with no changes
编辑:我还发现了一个奇怪的行为 - 如果动画差异是 true
,则单元格 不会 每次都重新加载。
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)"
}
}
我想你已经明白了。当您说 animatingDifferences
是 false
时,您是在要求可区分数据源表现得好像它是 而不是 可区分数据源一样。你是说:“跳过所有可区分的东西,只接受这个新数据。”换句话说,你说的相当于 reloadData()
。没有 created 新单元格(很容易通过日志证明),因为所有单元格都已经可见;但出于同样的原因,所有可见的单元格都被重新配置,这正是人们对 reloadData()
.
的期望
另一方面,当 animatingDifferences
为 true
时,diffable 数据源会认真考虑更改的内容,以便在必要时对其进行动画处理。因此,由于所有幕后工作,它知道何时可以避免重新加载单元格(如果不需要)(因为它可以改为移动单元格)。
确实,当 animatingDifferences
为 true
时,您可以应用一个反转单元格的快照,但是 configure
再也不会被调用,因为四处移动单元格是 所有需要完成的:
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)
}
有趣的是,我也用 shuffled
而不是 reversed
尝试了上面的方法,我发现 有时 一些单元格被重新配置。显然,diffable 数据源的主要目的不是不重新加载单元格;这只是一种副作用。
我创建了以下演示视图控制器以在最小示例中重现该问题。
在这里,我使用 UICollectionViewDiffableDataSource 将相同数据的快照重复应用到相同的集合视图,并且每次重新加载所有单元格时,即使没有任何更改。
我想知道这是一个错误,还是我“持有错误”。
其他用户似乎遇到了同样的问题,但他们没有提供足够的信息来准确重现错误: iOS UICollectionViewDiffableDataSource reloads all data with no changes
编辑:我还发现了一个奇怪的行为 - 如果动画差异是 true
,则单元格 不会 每次都重新加载。
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)"
}
}
我想你已经明白了。当您说 animatingDifferences
是 false
时,您是在要求可区分数据源表现得好像它是 而不是 可区分数据源一样。你是说:“跳过所有可区分的东西,只接受这个新数据。”换句话说,你说的相当于 reloadData()
。没有 created 新单元格(很容易通过日志证明),因为所有单元格都已经可见;但出于同样的原因,所有可见的单元格都被重新配置,这正是人们对 reloadData()
.
另一方面,当 animatingDifferences
为 true
时,diffable 数据源会认真考虑更改的内容,以便在必要时对其进行动画处理。因此,由于所有幕后工作,它知道何时可以避免重新加载单元格(如果不需要)(因为它可以改为移动单元格)。
确实,当 animatingDifferences
为 true
时,您可以应用一个反转单元格的快照,但是 configure
再也不会被调用,因为四处移动单元格是 所有需要完成的:
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)
}
有趣的是,我也用 shuffled
而不是 reversed
尝试了上面的方法,我发现 有时 一些单元格被重新配置。显然,diffable 数据源的主要目的不是不重新加载单元格;这只是一种副作用。