30

我很难找到 NSDiffableDataSourceSnapshot 的用途reloadItems(_:)

  • 如果我要求重新加载的项目与数据源中已经存在的项目不相等,我会崩溃:

    由于未捕获的异常“NSInternalInconsistencyException”而终止应用程序,原因:“尝试重新加载快照中不存在的项目标识符:ProjectName.ClassName

  • 但是,如果该项目等同于数据源中已经存在的项目,那么“重新加载”它的意义何在?

您可能会认为第二点的答案是:嗯,项目标识符对象的某些其他方面可能不是其等价性的一部分,但确实反映到单元格界面中。但我发现那不是真的。调用后reloadItems,表视图不反映更改。

因此,当我想更改一个项目时,我最终对快照所做的是insert要替换的项目之后,然后是delete原始项目的 a。没有快照replace方法,这正是我希望reloadItems的结果。

reloadItems(我对这些术语进行了Stack Overflow 搜索,但发现的很少——主要是几个对. ,有人发现这种方法有什么实际用途?)


好吧,没有什么比拥有一个可重复的最小示例更可取的了,所以这里有一个。

使用模板 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
    }
}

接下来,(假)表视图和可区分的数据源:

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 并且它booltrue. 所以现在设置一个按钮来调用这个操作方法,它试图bool通过使用来切换值reloadItems

@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)
        }
    }
}

事情就是这样。我对reloadItems一个 UUID 匹配但bool被切换的项目说:“这个对象的 isON 是假的”。但是当我问快照时,好吧,你有什么?它告诉我它唯一的项目标识符bool 仍然是 true

就是我要问的。如果快照不会获取 的新值bool,那么首先是reloadItems为了什么?

显然,我可以只替换一个不同的UniBool,即一个具有不同 UUID 的 UniBool。但是我不能打电话reloadItems;我们崩溃是因为 UniBool 还没有在数据中。我可以通过调用来解决这个问题insertremove这正是我解决它的方法。

但我的问题是:reloadItems如果不是为了这件事,那又是为了什么?

4

4 回答 4

8

(我已经对问题中展示的行为提出了一个错误,因为我认为这不是好的行为。但是,就目前情况而言,我认为我可以猜测这个想法的意图。)


当您告诉reload某个项目的快照时,它不会入您提供的项目的数据!它只是查看项目,作为识别数据源已经存在的项目的一种方式,您要求重新加载。

(因此,如果您提供的项目与数据源中已有的项目相同但不是 100% 相同,那么您提供的项目与数据源中已有的项目之间的“差异”根本不重要;数据永远不会告诉消息来源有什么不同。)

然后,当您apply将该快照保存到数据源时,数据源会告诉表视图重新加载相应的单元格。这会导致再次调用数据源的单元格提供程序函数。

好的,所以调用了数据源的单元格提供程序函数,使用了通常的三个参数——表格视图、索引路径和来自数据源的数据。但是我们刚才说过,来自数据源的数据没有改变。那么重新加载有什么意义呢?

显然,答案是单元格提供程序函数有望在别处寻找以获取(至少部分)要在新出列的单元格中显示的新数据。您应该有某种单元提供者查看的“后备存储”。例如,您可能正在维护一个字典,其中键是单元标识符类型,值是可能重新加载的额外信息。

这必须是合法的,因为根据定义,单元标识符类型是可散列的,因此可以用作字典键,而且单元标识符在数据中必须是唯一的,否则数据源将拒绝数据(通过崩溃)。并且查找将是即时的,因为这是一本字典。


这是一个完整的工作示例,您可以直接复制并粘贴到项目中。该表格描绘了三个名称以及一个星形,用户可以点击星形来填充或空出星形,表示喜欢或不喜欢。名称存储在 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[$0] = 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)
    }
}
于 2020-10-01T23:41:46.110 回答
6

我同意,根据您的新示例代码,它看起来像一个错误。当您将 a 添加reloadItems到快照时,它会正确触发数据源闭包以请求更新的单元格,但IdentifierType传递给闭包的项目是原始的,而不是reloadItems调用提供的新值。

如果我将您的UniBool结构更改为一个类,使其成为引用而不是值类型,那么事情会按预期工作(因为现在有一个 a 的实例UniBool而不是具有相同标识符的新实例)。

目前似乎有几种可能的解决方法:

  1. 使用引用而不是值类型IdentifierType
  2. 使用额外的后备存储,例如数组,并通过indexPath数据源闭包访问它。

我认为这两个都不理想。

有趣的是,在我更改UniBool为一个类之后,我尝试创建一个与现有实例UniBool相同uuid的新实例并重新加载它;代码崩溃,异常指出Invalid item identifier specified for reload;这对我来说听起来不对。只有hashValue应该重要,而不是实际的对象引用。原始对象和新对象都具有相同的hashValue返回==true


原始答案

reloadItems有效,但有两点很重要:

  1. 您必须从数据源的当前开始snapshot并调用reloadItems它。您无法创建新快照。

  2. 你不能依赖item传递给CellProvider闭包的任何东西identifier——它不代表你的支持模型(数组)中的最新数据。

第 2 点意味着您需要使用提供的indexPathitem.id从模型中获取更新的对象。

我创建了一个简单的示例,在表格行中显示当前时间;这是数据源结构:

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)
}
于 2020-09-27T21:22:16.007 回答
5

我发布了同样的问题,没有意识到。我通过首先将我的模型转换为类来完成这项工作。然后在调用“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)
}
于 2020-11-12T11:53:47.820 回答
2

我发现(通过Swift Senpai)更新这些 diffabledatasource 的方式取决于您的模型是类(通过引用传递)还是结构(通过值传递)。在通过引用传递中,您可以获取该项目,更新它,然后重新加载该项目:

// 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)

因此,上面的代码将适用于作为类的模型(该类需要显式实现 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)

这些对我有用。

于 2021-07-16T18:17:17.343 回答