Swift DiffableDataSource 进行插入和删除而不是重新加载
Swift DiffableDataSource make insert&delete instead of reload
我很难理解 DiffableDataSource 的工作原理。
我有这样的 ViewModel
struct ViewModel: Hashable {
var id: Int
var value: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
我有像上面的 ViewModele 这样的 cachedItems 填充的 tableView。当 API 响应到达时,我想添加一个新响应,删除丢失的响应,刷新 tableView 中已存在的 viewModel.value 个项目,最后订购它。
一切正常,除了一件事 - 重新加载项目。
我对 DiffableDataSource 的理解是,它比较 item.hash() 以检测项目是否已经存在,如果存在,那么如果 cachedItem != apiItem,它应该重新加载。
不幸的是,这不起作用,快照会删除和插入而不是重新加载。
DiffableDataSource 应该这样做吗?
当然,我有一个解决方案——为了让它工作,我需要遍历 cachedItems,当新项目包含相同的 id 时,我更新 cachedItem,然后我在没有动画的情况下应用快照,然后我终于可以在有动画的情况下应用快照deleting/inserting/ordering 动画。
但是这个解决方案似乎更像是一个 hack,而不是一个有效的代码。有没有更简洁的方法来实现这一目标?
更新:
有显示问题的代码。它应该在操场上工作。
例如。 items 和 newItems 包含 id == 0 的 viewModel。
哈希是相同的,因此 diffableDataSource 应该重新加载,因为字幕不同。
但是有可见的删除/插入而不是重新加载
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
let tableView = UITableView()
var diffableDataSource: UITableViewDiffableDataSource<Section, ViewModel>?
enum SelectesItems {
case items
case newItems
}
var selectedItems: SelectesItems = .items
let items: [ViewModel] = [ViewModel(id: 0, title: "Title1", subtitle: "Subtitle2"),
ViewModel(id: 1, title: "Title2", subtitle: "Subtitle2"),
ViewModel(id: 2, title: "Title3", subtitle: "Subtitle3"),
ViewModel(id: 3, title: "Title4", subtitle: "Subtitle4"),
ViewModel(id: 4, title: "Title5", subtitle: "Subtitle5")]
let newItems: [ViewModel] = [ViewModel(id: 0, title: "Title1", subtitle: "New Subtitle2"),
ViewModel(id: 2, title: "New Title 2", subtitle: "Subtitle3"),
ViewModel(id: 3, title: "Title4", subtitle: "Subtitle4"),
ViewModel(id: 4, title: "Title5", subtitle: "Subtitle5"),
ViewModel(id: 5, title: "Title6", subtitle: "Subtitle6")]
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CellID")
diffableDataSource = UITableViewDiffableDataSource<Section, ViewModel>(tableView: tableView, cellProvider: { (tableView, indexPath, viewModel) -> UITableViewCell? in
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "CellID")
cell.textLabel?.text = viewModel.title
cell.detailTextLabel?.text = viewModel.subtitle
return cell
})
applySnapshot(models: items)
let tgr = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tgr)
}
@objc func handleTap() {
switch selectedItems {
case .items:
applySnapshot(models: items)
selectedItems = .newItems
case .newItems:
applySnapshot(models: newItems)
selectedItems = .items
}
}
func applySnapshot(models: [ViewModel]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, ViewModel>()
snapshot.appendSections([.main])
snapshot.appendItems(models, toSection: .main)
diffableDataSource?.apply(snapshot, animatingDifferences: true)
}
}
enum Section {
case main
}
struct ViewModel: Hashable {
let id: Int
let title: String
let subtitle: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
是因为你对Hashable的实现不正确。
记住,Hashable 也意味着 Equatable——两者之间有着不可侵犯的关系。规则是两个相等的对象必须具有相等的散列值。但是在您的 ViewModel 中,"equal" 涉及比较所有三个属性,id
、title
和 subtitle
— 即使 hashValue
没有,因为您实现了 hash
.
换句话说,如果你实现hash
,你必须实现==
才能精确匹配:
struct ViewModel: Hashable {
let id: Int
let title: String
let subtitle: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func ==(lhs: ViewModel, rhs: ViewModel) -> Bool {
return lhs.id == rhs.id
}
}
如果您进行了更改,您会发现 table 视图动画的行为符合您的预期。
如果您也希望table视图了解基础数据实际上已更改的事实,那么您还必须调用reloadData
:
diffableDataSource?.apply(snapshot, animatingDifferences: true) {
self.tableView.reloadData()
}
(如果您有一些 other 原因希望 ViewModel 的 Equatable 继续涉及所有三个属性,那么您需要 two 类型,一种用于简单明了的相等比较,另一种用于涉及 Hashable 的上下文,例如可区分数据源、集合和字典键。)
我建议阅读 Apple 的 this 文章,它完美地解释了您面临的所有问题。总之,如果你想实现完美的更新你需要:
- 确保
UITableViewDiffableDataSource.ItemIdentifierType
使用项目标识符而不是项目。
- 您手动跟踪更新并将它们添加到快照更改中:
例如:
snapshot.reloadItems(updates) // updates is an array of item identifier that got updated
对于我的项目,我只是将所有新项目与以前的项目进行比较,然后再进行相等更改。
我很难理解 DiffableDataSource 的工作原理。 我有这样的 ViewModel
struct ViewModel: Hashable {
var id: Int
var value: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
我有像上面的 ViewModele 这样的 cachedItems 填充的 tableView。当 API 响应到达时,我想添加一个新响应,删除丢失的响应,刷新 tableView 中已存在的 viewModel.value 个项目,最后订购它。 一切正常,除了一件事 - 重新加载项目。
我对 DiffableDataSource 的理解是,它比较 item.hash() 以检测项目是否已经存在,如果存在,那么如果 cachedItem != apiItem,它应该重新加载。 不幸的是,这不起作用,快照会删除和插入而不是重新加载。
DiffableDataSource 应该这样做吗?
当然,我有一个解决方案——为了让它工作,我需要遍历 cachedItems,当新项目包含相同的 id 时,我更新 cachedItem,然后我在没有动画的情况下应用快照,然后我终于可以在有动画的情况下应用快照deleting/inserting/ordering 动画。
但是这个解决方案似乎更像是一个 hack,而不是一个有效的代码。有没有更简洁的方法来实现这一目标?
更新:
有显示问题的代码。它应该在操场上工作。 例如。 items 和 newItems 包含 id == 0 的 viewModel。 哈希是相同的,因此 diffableDataSource 应该重新加载,因为字幕不同。 但是有可见的删除/插入而不是重新加载
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
let tableView = UITableView()
var diffableDataSource: UITableViewDiffableDataSource<Section, ViewModel>?
enum SelectesItems {
case items
case newItems
}
var selectedItems: SelectesItems = .items
let items: [ViewModel] = [ViewModel(id: 0, title: "Title1", subtitle: "Subtitle2"),
ViewModel(id: 1, title: "Title2", subtitle: "Subtitle2"),
ViewModel(id: 2, title: "Title3", subtitle: "Subtitle3"),
ViewModel(id: 3, title: "Title4", subtitle: "Subtitle4"),
ViewModel(id: 4, title: "Title5", subtitle: "Subtitle5")]
let newItems: [ViewModel] = [ViewModel(id: 0, title: "Title1", subtitle: "New Subtitle2"),
ViewModel(id: 2, title: "New Title 2", subtitle: "Subtitle3"),
ViewModel(id: 3, title: "Title4", subtitle: "Subtitle4"),
ViewModel(id: 4, title: "Title5", subtitle: "Subtitle5"),
ViewModel(id: 5, title: "Title6", subtitle: "Subtitle6")]
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CellID")
diffableDataSource = UITableViewDiffableDataSource<Section, ViewModel>(tableView: tableView, cellProvider: { (tableView, indexPath, viewModel) -> UITableViewCell? in
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "CellID")
cell.textLabel?.text = viewModel.title
cell.detailTextLabel?.text = viewModel.subtitle
return cell
})
applySnapshot(models: items)
let tgr = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tgr)
}
@objc func handleTap() {
switch selectedItems {
case .items:
applySnapshot(models: items)
selectedItems = .newItems
case .newItems:
applySnapshot(models: newItems)
selectedItems = .items
}
}
func applySnapshot(models: [ViewModel]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, ViewModel>()
snapshot.appendSections([.main])
snapshot.appendItems(models, toSection: .main)
diffableDataSource?.apply(snapshot, animatingDifferences: true)
}
}
enum Section {
case main
}
struct ViewModel: Hashable {
let id: Int
let title: String
let subtitle: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
是因为你对Hashable的实现不正确。
记住,Hashable 也意味着 Equatable——两者之间有着不可侵犯的关系。规则是两个相等的对象必须具有相等的散列值。但是在您的 ViewModel 中,"equal" 涉及比较所有三个属性,id
、title
和 subtitle
— 即使 hashValue
没有,因为您实现了 hash
.
换句话说,如果你实现hash
,你必须实现==
才能精确匹配:
struct ViewModel: Hashable {
let id: Int
let title: String
let subtitle: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func ==(lhs: ViewModel, rhs: ViewModel) -> Bool {
return lhs.id == rhs.id
}
}
如果您进行了更改,您会发现 table 视图动画的行为符合您的预期。
如果您也希望table视图了解基础数据实际上已更改的事实,那么您还必须调用reloadData
:
diffableDataSource?.apply(snapshot, animatingDifferences: true) {
self.tableView.reloadData()
}
(如果您有一些 other 原因希望 ViewModel 的 Equatable 继续涉及所有三个属性,那么您需要 two 类型,一种用于简单明了的相等比较,另一种用于涉及 Hashable 的上下文,例如可区分数据源、集合和字典键。)
我建议阅读 Apple 的 this 文章,它完美地解释了您面临的所有问题。总之,如果你想实现完美的更新你需要:
- 确保
UITableViewDiffableDataSource.ItemIdentifierType
使用项目标识符而不是项目。 - 您手动跟踪更新并将它们添加到快照更改中:
例如:
对于我的项目,我只是将所有新项目与以前的项目进行比较,然后再进行相等更改。snapshot.reloadItems(updates) // updates is an array of item identifier that got updated