ios - NSDiffableDataSourceSnapshot `reloadItems` 有什么用?

标签 ios swift nsdiffabledatasourcesnapshot

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

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

    Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempted to reload item identifier that does not exist in the snapshot: ProjectName.ClassName


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

  • 您可能会认为第二点的答案是:嗯,项目标识符对象的某些其他方面可能不是其等价性的一部分,但确实反射(reflect)到单元格界面中。但我发现那不是真的。调用 reloadItems 后, TableView 不反射(reflect)更改。
    因此,当我想更改一个项目时,我最终对快照所做的是 insert在要更换的项目之后,然后是 delete的原始项目。没有快照replace方法,这正是我所希望的 reloadItems结果会是。
    (我对这些术语进行了 Stack Overflow 搜索,但发现很少——主要是几个对 reloadItems 的特定用途感到困惑的问题,例如 How to update a table cell using diffable UITableView 。所以我以更笼统的形式问,什么实用有没有人发现这种方法?)

    好吧,没有什么比拥有一个可重复的最小示例更可取的了,所以这里有一个。
    使用模板 ViewController 制作一个普通的 iOS 项目,并将此代码添加到 ViewController。
    我一 block 一 block 来。首先,我们有一个结构将用作我们的项目标识符。 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
        }
    }
    
    接下来,(假) TableView 和可区分的数据源:
    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仍然是真的。
    这就是我要问的。如果快照不会获取 bool 的新值, 什么是 reloadItems首先是为了?
    显然,我可以只替换一个不同的 UniBool,即一个具有不同 UUID 的 UniBool。但是我不能调用reloadItems ;我们崩溃是因为 UniBool 还没有在数据中。我可以通过调用 insert 来解决这个问题。后跟 remove ,这正是我解决它的方法。
    但我的问题是:那么 reloadItems 是什么?因为,如果不是为了这件事呢?

    最佳答案

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

    当您将快照告诉 reload某个项目,它不会读入您提供的项目的数据!它只是查看项目,作为识别数据源中已经存在的项目的一种方式,您要求重新加载。
    (因此,如果您提供的项目与数据源中已有的项目相同但不是 100% 相同,那么您提供的项目与数据源中已有的项目之间的“差异”将 根本不重要 ;永远不会告诉数据源有什么不同。)
    当你然后apply该快照到数据源,数据源告诉 TableView 重新加载相应的单元格。这会导致再次调用数据源的单元格提供程序函数。
    好的,所以调用了数据源的单元格提供程序函数,使用了通常的三个参数——表格 View 、索引路径和来自数据源的数据。但是我们刚才说过,来自数据源的数据没有改变。那么重新加载有什么意义呢?
    显然,答案是单元格提供程序函数有望在别处寻找以获取(至少部分)要在新出列的单元格中显示的新数据。您应该有某种单元提供者查看的“后备存储”。例如,您可能正在维护一个字典,其中键是单元标识符类型,值是可能重新加载的额外信息。
    这必须是合法的,因为根据定义,单元标识符类型是可散列的,因此可以用作字典键,而且单元标识符在数据中必须是唯一的,否则数据源会拒绝数据(通过崩溃)。并且查找将是即时的,因为这是一本字典。

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

    关于ios - NSDiffableDataSourceSnapshot `reloadItems` 有什么用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64081701/

    相关文章:

    iphone - 为 UIImagePicker 提供您自己的 UIImage。

    ios - 如何保持表格 View 滚动以停止重复使用单元格

    ios - Swift 函数式编程 - 有没有比两个 map 调用更好的方法来翻译嵌套的 for 循环

    ios - 更改快照 animatedDifferences 值时如何处理不一致的行为?

    ios - UICollectionViewDiffableDataSource 正在替换数据而不是更新

    iOS13 DiffableDataSource 无效参数不满足 : indexPath || ignoreInvalidItems

    objective-c - 当 alpha 为 1.0 时,OpenGL ES Faces 看起来是透明的?

    iphone - Objective-C 何时在@interface 中声明哪些方法

    iOS 崩溃日志 iPad sqlite 互斥锁

    xcode - 在不同 NSViewController 之间传递数据 swift OS X