NSDiffableDataSourceSnapshot `reloadItems` 有什么用?
What is NSDiffableDataSourceSnapshot `reloadItems` for?
我很难找到 NSDiffableDataSourceSnapshot 的用途 reloadItems(_:)
:
如果我要求重新加载的项目不等于table 到数据源中已经存在的项目,我会崩溃:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempted to reload item identifier that does not exist in the snapshot: ProjectName.ClassName
但是如果项目是等于table到数据源中已经存在的项目,那么“重新加载”有什么意义》吗?
您可能认为第二点的答案是:好吧,项目标识符对象的某些其他方面可能不是其等同性的一部分,但会反映到单元格界面中。但我发现那不是真的。调用 reloadItems
后,table 视图 未 反映更改。
所以当我想更改一个项目时,我最终对快照所做的是在要替换的项目之后添加 insert
,然后是原始项目的 delete
。没有快照 replace
方法,这正是我希望 reloadItems
会变成的方法。
(我对这些术语进行了 Stack Overflow 搜索,发现很少——大部分只是一些对 reloadItems
的特定用途感到困惑的问题,例如 How to update a table cell using diffable UITableView。所以我以更笼统的形式提问,任何人 发现这种方法有什么实际用途?)
好吧,没有什么比玩一个最小的可重现示例更好的了,所以这里有一个。
使用其模板 ViewController 创建一个普通的 iOS 项目,并将此代码添加到 ViewController.
我会一点一点的。首先,我们有一个结构将用作我们的项目标识符。 UUID 是唯一的部分,因此等同性和哈希性仅取决于它:
struct UniBool : Hashable {
let uuid : UUID
var bool : Bool
// equatability and hashability agree, only the UUID matters
func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
}
static func ==(lhs:Self, rhs:Self) -> Bool {
lhs.uuid == rhs.uuid
}
}
接下来,(假)table 视图和 diffable 数据源:
let tableView = UITableView(frame: .zero, style: .plain)
var datasource : UITableViewDiffableDataSource<String,UniBool>!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
self.datasource = UITableViewDiffableDataSource<String,UniBool>(tableView: self.tableView) { tv, ip, isOn in
let cell = tv.dequeueReusableCell(withIdentifier: "cell", for: ip)
return cell
}
var snap = NSDiffableDataSourceSnapshot<String,UniBool>()
snap.appendSections(["Dummy"])
snap.appendItems([UniBool(uuid: UUID(), bool: true)])
self.datasource.apply(snap, animatingDifferences: false)
}
所以我们的diffable数据源中只有一个UniBool,它的bool
是true
。因此,现在设置一个按钮来调用此操作方法,该方法尝试使用 reloadItems
:
来切换 bool
值
@IBAction func testReload() {
if let unibool = self.datasource.itemIdentifier(for: IndexPath(row: 0, section: 0)) {
var snap = self.datasource.snapshot()
var unibool = unibool
unibool.bool = !unibool.bool
snap.reloadItems([unibool]) // this is the key line I'm trying to test!
print("this object's isOn is", unibool.bool)
print("but looking right at the snapshot, isOn is", snap.itemIdentifiers[0].bool)
delay(0.3) {
self.datasource.apply(snap, animatingDifferences: false)
}
}
}
事情是这样的。我用一个 UUID 匹配但 bool
被切换的项目对 reloadItems
说:“这个对象的 isON 是假的”。但是当我问快照时,好吧,你有什么?它告诉我它唯一的项目标识符 bool
仍然是 true.
就是我要问的问题。如果快照不打算获取 bool
的新值,那么 reloadItems
首先是什么?
显然我可以用 不同的 UniBool 代替,即具有不同 UUID 的一个。但是我不能打电话给 reloadItems
;我们崩溃是因为 UniBool 不在数据中。我可以通过调用 insert
然后调用 remove
来解决这个问题,这正是我解决它的方法。
但我的问题是:那么 reloadItems
是为了什么,如果不是为了这件事?
根据您的新示例代码,我同意,它看起来像一个错误。当您将 reloadItems
添加到快照时,它会正确触发数据源闭包以请求更新的单元格,但是传递给闭包的 IdentifierType
项目是原始值,而不是提供的新值reloadItems
电话。
如果我将你的 UniBool
结构更改为 class 以便它是一个引用而不是值类型,那么事情会按预期工作(因为现在有一个 UniBool
而不是具有相同标识符的新的)。
目前看来有几种可能work-arounds:
- 为
IdentifierType
使用引用而不是值类型
- 使用额外的后备存储,例如数组,并通过数据源闭包中的
indexPath
访问它。
我认为这两个都不理想。
有趣的是,在我将 UniBool
更改为 class 之后,我尝试创建一个 UniBool
的新实例,它与现有实例具有相同的 uuid
并重新加载那;代码崩溃并出现异常,指出 为重新加载指定的项目标识符无效 ;这对我来说听起来不对;只有 hashValue
应该重要,而不是实际的对象引用。原始对象和新对象都具有相同的 hashValue
并且 ==
返回 true
。
原回答
reloadItems
有效,但有两点很重要:
您必须从数据源的当前 snapshot
开始并对其调用 reloadItems
。您无法创建新快照。
你不能依赖传递给 CellProvider
闭包 的 item
除了 identifier
- 它不代表来自支持模型(数组)的最新数据。
第 2 点意味着您需要使用提供的 indexPath
或 item.id
从模型中获取更新的对象。
我创建了一个简单的 example,它在 table 行中显示当前时间;这是数据源结构:
struct RowData: Hashable {
var id: UUID = UUID()
var name: String
private let possibleColors: [UIColor] = [.yellow,.orange,.cyan]
var timeStamp = Date()
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
static func ==(lhs: RowData, rhs: RowData) -> Bool {
return lhs.id == rhs.id
}
}
请注意,尽管 hash
函数仅使用 id
属性,但也有必要重写 ==
,否则您将因标识符无效而崩溃您尝试重新加载该行。
每秒都会重新加载随机选择的行。当您 运行 代码时,您会看到时间在那些随机选择的行上更新。
这是使用reloadItems
的代码:
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
guard let datasource = self.tableview.dataSource as? UITableViewDiffableDataSource<Section,RowData> else {
return
}
var snapshot = datasource.snapshot()
var rowIdentifers = Set<RowData>()
for _ in 0...Int.random(in: 1...self.arrItems.count) {
let randomIndex = Int.random(in: 0...self.arrItems.count-1)
self.arrItems[randomIndex].timeStamp = Date()
rowIdentifers.insert(self.arrItems[randomIndex])
}
snapshot.reloadItems(Array(rowIdentifers))
datasource.apply(snapshot)
}
(我已经针对问题中展示的行为提交了错误,因为我认为这不是好的行为。但是,就目前情况而言,我想我可以猜测这个想法的意图成为。)
当您将快照告诉 reload
某个项目时,它 不会 读取您提供的项目的数据!它只是查看项目,作为识别什么项目的一种方式,已经在数据源中,您要求重新加载。
(因此,如果您提供的项目与数据源中已有的项目等同table但不是 100% 相同,则您提供的项目与数据源中已有的项目之间的“差异”数据源将完全无关紧要;数据源永远不会被告知有任何不同。)
当您 apply
将该快照发送到数据源时,数据源会告诉 table 视图重新加载相应的单元格。这导致再次调用数据源的 单元格提供程序函数 。
OK,数据源的单元格提供程序函数被调用,使用通常的三个参数 — table 视图、索引路径和来自数据源的数据。但是我们刚才说数据源的数据没有改变。那么重新加载有什么意义呢?
答案显然是,单元格提供程序函数预计会在其他地方查找以获取(至少部分)要在新出列的单元格中显示的新数据.您应该拥有某种“后备存储”,供手机提供商查看。例如,您可能正在维护一个字典,其中键是单元格标识符类型,值是可能重新加载的额外信息。
这必须是合法的,因为根据定义,单元格标识符类型是可散列的,因此可以用作字典键,而且单元格标识符在数据中必须是唯一的,否则数据源将拒绝数据(通过崩溃)。而且查找将是即时的,因为这是一本字典。
这是一个完整的工作示例,您可以直接复制并粘贴到项目中。 table 描绘了三个名字和一个星号,用户可以点击星号来填充或空出星号,表示最喜欢或 not-favorite。名称存储在 diffable 数据源中,但收藏状态存储在外部后备存储中。
extension UIResponder {
func next<T:UIResponder>(ofType: T.Type) -> T? {
let r = self.next
if let r = r as? T ?? r?.next(ofType: T.self) {
return r
} else {
return nil
}
}
}
class TableViewController: UITableViewController {
var backingStore = [String:Bool]()
var datasource : UITableViewDiffableDataSource<String,String>!
override func viewDidLoad() {
super.viewDidLoad()
let cellID = "cell"
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
self.datasource = UITableViewDiffableDataSource<String,String>(tableView:self.tableView) {
tableView, indexPath, name in
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
var config = cell.defaultContentConfiguration()
config.text = name
cell.contentConfiguration = config
var accImageView = cell.accessoryView as? UIImageView
if accImageView == nil {
let iv = UIImageView()
iv.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(self.starTapped))
iv.addGestureRecognizer(tap)
cell.accessoryView = iv
accImageView = iv
}
let starred = self.backingStore[name, default:false]
accImageView?.image = UIImage(systemName: starred ? "star.fill" : "star")
accImageView?.sizeToFit()
return cell
}
var snap = NSDiffableDataSourceSnapshot<String,String>()
snap.appendSections(["Dummy"])
let names = ["Manny", "Moe", "Jack"]
snap.appendItems(names)
self.datasource.apply(snap, animatingDifferences: false)
names.forEach {
self.backingStore[[=10=]] = false
}
}
@objc func starTapped(_ gr:UIGestureRecognizer) {
guard let cell = gr.view?.next(ofType: UITableViewCell.self) else {return}
guard let ip = self.tableView.indexPath(for: cell) else {return}
guard let name = self.datasource.itemIdentifier(for: ip) else {return}
guard let isFavorite = self.backingStore[name] else {return}
self.backingStore[name] = !isFavorite
var snap = self.datasource.snapshot()
snap.reloadItems([name])
self.datasource.apply(snap, animatingDifferences: false)
}
}
我发布了 same question,没有意识到。我通过首先将我的模型转换为 类 来实现这一点。然后在调用 'reloadItems'.
之后调用 'applySnapshot'
func toggleSelectedStateForItem(at indexPath: IndexPath, animate: Bool = true) {
let item = dataSource.itemIdentifier(for: indexPath)!
var snapshot = dataSource.snapshot()
item.isSelected = !item.isSelected
snapshot.reloadItems([item])
dataSource.apply(snapshot)
}
我发现(通过 Swift Senpai)您更新这些 diffabledatasource 的方式取决于您的模型是 class(按引用传递)还是结构(按值传递)。在通过引用传递你可以获取项目,更新它,然后重新加载项目:
// Model is a class compliant with Hasable and Equatable, name String property
guard let selectedItem = dataSource.itemIdentifier(for: indexPath) else { return}
// modify item
selectedItem.name = "new name"
// update the snapshot
var newSnapShot = dataSource.snapshot()
newSnapshot.reloadItems([selectedItem])
dataSource.apply(newSnapshot)
所以上面的代码将适用于 class 模型(class 需要显式实现 hast(into:) 和 ==(lhs:rhs:))。
另一方面,结构要求您复制项目、更新它,然后插入更新的项目并从快照中删除旧项目。
// Model is a struct with name String property
guard let selectedItem = dataSource.itemIdentifier(for: indexPath) else { return}
// update the item
var updatedSelectedItem = selectedItem
updatedSelectedItem.name = "new name"
// update snapshot
var newSnapShot = dataSource.snapshot()
newSnapshot.insertItems([updatedSelectedItem], beforeItem: selectedItem)
newSnapshot.deleteItems([selectedItem])
dataSource.apply(newSnapshot)
这些对我有用。
我很难找到 NSDiffableDataSourceSnapshot 的用途 reloadItems(_:)
:
如果我要求重新加载的项目不等于table 到数据源中已经存在的项目,我会崩溃:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempted to reload item identifier that does not exist in the snapshot: ProjectName.ClassName
但是如果项目是等于table到数据源中已经存在的项目,那么“重新加载”有什么意义》吗?
您可能认为第二点的答案是:好吧,项目标识符对象的某些其他方面可能不是其等同性的一部分,但会反映到单元格界面中。但我发现那不是真的。调用 reloadItems
后,table 视图 未 反映更改。
所以当我想更改一个项目时,我最终对快照所做的是在要替换的项目之后添加 insert
,然后是原始项目的 delete
。没有快照 replace
方法,这正是我希望 reloadItems
会变成的方法。
(我对这些术语进行了 Stack Overflow 搜索,发现很少——大部分只是一些对 reloadItems
的特定用途感到困惑的问题,例如 How to update a table cell using diffable UITableView。所以我以更笼统的形式提问,任何人 发现这种方法有什么实际用途?)
好吧,没有什么比玩一个最小的可重现示例更好的了,所以这里有一个。
使用其模板 ViewController 创建一个普通的 iOS 项目,并将此代码添加到 ViewController.
我会一点一点的。首先,我们有一个结构将用作我们的项目标识符。 UUID 是唯一的部分,因此等同性和哈希性仅取决于它:
struct UniBool : Hashable {
let uuid : UUID
var bool : Bool
// equatability and hashability agree, only the UUID matters
func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
}
static func ==(lhs:Self, rhs:Self) -> Bool {
lhs.uuid == rhs.uuid
}
}
接下来,(假)table 视图和 diffable 数据源:
let tableView = UITableView(frame: .zero, style: .plain)
var datasource : UITableViewDiffableDataSource<String,UniBool>!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
self.datasource = UITableViewDiffableDataSource<String,UniBool>(tableView: self.tableView) { tv, ip, isOn in
let cell = tv.dequeueReusableCell(withIdentifier: "cell", for: ip)
return cell
}
var snap = NSDiffableDataSourceSnapshot<String,UniBool>()
snap.appendSections(["Dummy"])
snap.appendItems([UniBool(uuid: UUID(), bool: true)])
self.datasource.apply(snap, animatingDifferences: false)
}
所以我们的diffable数据源中只有一个UniBool,它的bool
是true
。因此,现在设置一个按钮来调用此操作方法,该方法尝试使用 reloadItems
:
bool
值
@IBAction func testReload() {
if let unibool = self.datasource.itemIdentifier(for: IndexPath(row: 0, section: 0)) {
var snap = self.datasource.snapshot()
var unibool = unibool
unibool.bool = !unibool.bool
snap.reloadItems([unibool]) // this is the key line I'm trying to test!
print("this object's isOn is", unibool.bool)
print("but looking right at the snapshot, isOn is", snap.itemIdentifiers[0].bool)
delay(0.3) {
self.datasource.apply(snap, animatingDifferences: false)
}
}
}
事情是这样的。我用一个 UUID 匹配但 bool
被切换的项目对 reloadItems
说:“这个对象的 isON 是假的”。但是当我问快照时,好吧,你有什么?它告诉我它唯一的项目标识符 bool
仍然是 true.
就是我要问的问题。如果快照不打算获取 bool
的新值,那么 reloadItems
首先是什么?
显然我可以用 不同的 UniBool 代替,即具有不同 UUID 的一个。但是我不能打电话给 reloadItems
;我们崩溃是因为 UniBool 不在数据中。我可以通过调用 insert
然后调用 remove
来解决这个问题,这正是我解决它的方法。
但我的问题是:那么 reloadItems
是为了什么,如果不是为了这件事?
根据您的新示例代码,我同意,它看起来像一个错误。当您将 reloadItems
添加到快照时,它会正确触发数据源闭包以请求更新的单元格,但是传递给闭包的 IdentifierType
项目是原始值,而不是提供的新值reloadItems
电话。
如果我将你的 UniBool
结构更改为 class 以便它是一个引用而不是值类型,那么事情会按预期工作(因为现在有一个 UniBool
而不是具有相同标识符的新的)。
目前看来有几种可能work-arounds:
- 为
IdentifierType
使用引用而不是值类型
- 使用额外的后备存储,例如数组,并通过数据源闭包中的
indexPath
访问它。
我认为这两个都不理想。
有趣的是,在我将 UniBool
更改为 class 之后,我尝试创建一个 UniBool
的新实例,它与现有实例具有相同的 uuid
并重新加载那;代码崩溃并出现异常,指出 为重新加载指定的项目标识符无效 ;这对我来说听起来不对;只有 hashValue
应该重要,而不是实际的对象引用。原始对象和新对象都具有相同的 hashValue
并且 ==
返回 true
。
原回答
reloadItems
有效,但有两点很重要:
您必须从数据源的当前
snapshot
开始并对其调用reloadItems
。您无法创建新快照。你不能依赖传递给
CellProvider
闭包 的item
除了identifier
- 它不代表来自支持模型(数组)的最新数据。
第 2 点意味着您需要使用提供的 indexPath
或 item.id
从模型中获取更新的对象。
我创建了一个简单的 example,它在 table 行中显示当前时间;这是数据源结构:
struct RowData: Hashable {
var id: UUID = UUID()
var name: String
private let possibleColors: [UIColor] = [.yellow,.orange,.cyan]
var timeStamp = Date()
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
static func ==(lhs: RowData, rhs: RowData) -> Bool {
return lhs.id == rhs.id
}
}
请注意,尽管 hash
函数仅使用 id
属性,但也有必要重写 ==
,否则您将因标识符无效而崩溃您尝试重新加载该行。
每秒都会重新加载随机选择的行。当您 运行 代码时,您会看到时间在那些随机选择的行上更新。
这是使用reloadItems
的代码:
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
guard let datasource = self.tableview.dataSource as? UITableViewDiffableDataSource<Section,RowData> else {
return
}
var snapshot = datasource.snapshot()
var rowIdentifers = Set<RowData>()
for _ in 0...Int.random(in: 1...self.arrItems.count) {
let randomIndex = Int.random(in: 0...self.arrItems.count-1)
self.arrItems[randomIndex].timeStamp = Date()
rowIdentifers.insert(self.arrItems[randomIndex])
}
snapshot.reloadItems(Array(rowIdentifers))
datasource.apply(snapshot)
}
(我已经针对问题中展示的行为提交了错误,因为我认为这不是好的行为。但是,就目前情况而言,我想我可以猜测这个想法的意图成为。)
当您将快照告诉 reload
某个项目时,它 不会 读取您提供的项目的数据!它只是查看项目,作为识别什么项目的一种方式,已经在数据源中,您要求重新加载。
(因此,如果您提供的项目与数据源中已有的项目等同table但不是 100% 相同,则您提供的项目与数据源中已有的项目之间的“差异”数据源将完全无关紧要;数据源永远不会被告知有任何不同。)
当您 apply
将该快照发送到数据源时,数据源会告诉 table 视图重新加载相应的单元格。这导致再次调用数据源的 单元格提供程序函数 。
OK,数据源的单元格提供程序函数被调用,使用通常的三个参数 — table 视图、索引路径和来自数据源的数据。但是我们刚才说数据源的数据没有改变。那么重新加载有什么意义呢?
答案显然是,单元格提供程序函数预计会在其他地方查找以获取(至少部分)要在新出列的单元格中显示的新数据.您应该拥有某种“后备存储”,供手机提供商查看。例如,您可能正在维护一个字典,其中键是单元格标识符类型,值是可能重新加载的额外信息。
这必须是合法的,因为根据定义,单元格标识符类型是可散列的,因此可以用作字典键,而且单元格标识符在数据中必须是唯一的,否则数据源将拒绝数据(通过崩溃)。而且查找将是即时的,因为这是一本字典。
这是一个完整的工作示例,您可以直接复制并粘贴到项目中。 table 描绘了三个名字和一个星号,用户可以点击星号来填充或空出星号,表示最喜欢或 not-favorite。名称存储在 diffable 数据源中,但收藏状态存储在外部后备存储中。
extension UIResponder {
func next<T:UIResponder>(ofType: T.Type) -> T? {
let r = self.next
if let r = r as? T ?? r?.next(ofType: T.self) {
return r
} else {
return nil
}
}
}
class TableViewController: UITableViewController {
var backingStore = [String:Bool]()
var datasource : UITableViewDiffableDataSource<String,String>!
override func viewDidLoad() {
super.viewDidLoad()
let cellID = "cell"
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
self.datasource = UITableViewDiffableDataSource<String,String>(tableView:self.tableView) {
tableView, indexPath, name in
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
var config = cell.defaultContentConfiguration()
config.text = name
cell.contentConfiguration = config
var accImageView = cell.accessoryView as? UIImageView
if accImageView == nil {
let iv = UIImageView()
iv.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(self.starTapped))
iv.addGestureRecognizer(tap)
cell.accessoryView = iv
accImageView = iv
}
let starred = self.backingStore[name, default:false]
accImageView?.image = UIImage(systemName: starred ? "star.fill" : "star")
accImageView?.sizeToFit()
return cell
}
var snap = NSDiffableDataSourceSnapshot<String,String>()
snap.appendSections(["Dummy"])
let names = ["Manny", "Moe", "Jack"]
snap.appendItems(names)
self.datasource.apply(snap, animatingDifferences: false)
names.forEach {
self.backingStore[[=10=]] = false
}
}
@objc func starTapped(_ gr:UIGestureRecognizer) {
guard let cell = gr.view?.next(ofType: UITableViewCell.self) else {return}
guard let ip = self.tableView.indexPath(for: cell) else {return}
guard let name = self.datasource.itemIdentifier(for: ip) else {return}
guard let isFavorite = self.backingStore[name] else {return}
self.backingStore[name] = !isFavorite
var snap = self.datasource.snapshot()
snap.reloadItems([name])
self.datasource.apply(snap, animatingDifferences: false)
}
}
我发布了 same question,没有意识到。我通过首先将我的模型转换为 类 来实现这一点。然后在调用 'reloadItems'.
之后调用 'applySnapshot'func toggleSelectedStateForItem(at indexPath: IndexPath, animate: Bool = true) {
let item = dataSource.itemIdentifier(for: indexPath)!
var snapshot = dataSource.snapshot()
item.isSelected = !item.isSelected
snapshot.reloadItems([item])
dataSource.apply(snapshot)
}
我发现(通过 Swift Senpai)您更新这些 diffabledatasource 的方式取决于您的模型是 class(按引用传递)还是结构(按值传递)。在通过引用传递你可以获取项目,更新它,然后重新加载项目:
// Model is a class compliant with Hasable and Equatable, name String property
guard let selectedItem = dataSource.itemIdentifier(for: indexPath) else { return}
// modify item
selectedItem.name = "new name"
// update the snapshot
var newSnapShot = dataSource.snapshot()
newSnapshot.reloadItems([selectedItem])
dataSource.apply(newSnapshot)
所以上面的代码将适用于 class 模型(class 需要显式实现 hast(into:) 和 ==(lhs:rhs:))。
另一方面,结构要求您复制项目、更新它,然后插入更新的项目并从快照中删除旧项目。
// Model is a struct with name String property
guard let selectedItem = dataSource.itemIdentifier(for: indexPath) else { return}
// update the item
var updatedSelectedItem = selectedItem
updatedSelectedItem.name = "new name"
// update snapshot
var newSnapShot = dataSource.snapshot()
newSnapshot.insertItems([updatedSelectedItem], beforeItem: selectedItem)
newSnapshot.deleteItems([selectedItem])
dataSource.apply(newSnapshot)
这些对我有用。