NSDiffableDataSourceSnapshot`reloadItems`是做什么用的? [英] What is NSDiffableDataSourceSnapshot `reloadItems` for?

查看:184
本文介绍了NSDiffableDataSourceSnapshot`reloadItems`是做什么用的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

  • 如果我要重新加载的项目不能等同于数据源中已经存在的项目,则会崩溃.

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

您可能会认为,第二点的答案是:好吧,项目标识对象的其他方面可能不是其等同性的一部分,但确实反映在单元界面中.但是我发现那是不对的.调用reloadItems后,表视图不会反映更改.

因此,当我要更改项目时,最终要对快照执行的操作是要替换的项目之后的insert,然后是原始项目的delete.没有快照replace方法,这就是我希望reloadItems会变成的样子.

(我对这些术语进行了堆栈溢出搜索,却发现很少-大多数只是几个问题,这些问题困扰了reloadItems的特殊用法,例如bool 仍然为真.

是我要问的.如果快照不准备使用新的bool值,那么reloadItems首先是什么?

很明显,我可以只用一个不同 UniBool代替,即用一个不同的UUID.但是后来我不能打电话给reloadItems;我们崩溃,因为该数据中还没有UniBool.我可以通过依次调用insertremove来解决此问题,而这正是我的解决方法.

但是我的问题是:reloadItems是什么意思,如果不是因为这件事呢?

解决方案

(我对问题中展示的行为提出了错误,因为我认为这不是好行为.但是,就目前情况而言,我认为我可以提供一个关于这个想法打算是什么的猜测.)


当您将某个快照告知reload某个项目时,它不会读取您提供的项目的数据!它只是查看该项目,作为识别什么项目,已经在数据源中,您要重新加载. >

(因此,如果您提供的商品与数据源中已有的商品相当,但并非100%相同,则您提供的商品与数据源中已有的商品之间的差异"将没关系;永远不会告诉数据源有什么不同.)

然后,当您将该快照快照apply到数据源时,该数据源将通知表视图重新加载相应的单元格.这导致再次调用数据源的单元提供程序功能.

好,因此将使用通常的三个参数调用数据源的单元提供程序功能-表视图,索引路径和数据源中的数据.但是我们只是说来自数据源的数据没有改变.那么重新加载的意义到底是什么?

显然,答案是,单元提供程序功能看起来应该在其他地方,以获取(至少部分)要在新出队的单元中显示的新数据.您应该拥有某种类型的后备存储".单元提供商查看的内容.例如,您可能正在维护一个词典,其中的键是单元格标识符类型,值是可能会重新加载的额外信息.

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


这是一个完整的工作示例,您可以直接将其复制并粘贴到项目中.该表描绘了三个名称以及一个星号,用户可以点击该星号以将星号填充或填充为空,以表示喜欢或不喜欢.名称存储在可扩散数据源中,但收藏夹状态存储在外部后备存储中.

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

I'm having difficulty finding the use of NSDiffableDataSourceSnapshot reloadItems(_:):

  • If the item I ask to reload is not equatable to an item that is already present in the data source, I crash.

  • But if the item is equatable to an item that is already present in the data source, then what's the point of "reloading" it?

You might think the answer to the second point is: well, there might be some other aspect of the item identifier object that is not part of its equatability but does reflect into the cell interface. But what I find is that that's not true; after calling reloadItems, the table view does not reflect the change.

So when I want to change an item, what I end up doing with the snapshot is an insert after the item to be replaced and then a delete of the original item. There is no snapshot replace method, which is what I was hoping reloadItems would turn out to be.

(I did a Stack Overflow search on those terms and found very little — mostly just a couple of questions that puzzled over particular uses of reloadItems, such as How to update a table cell using diffable UITableView. So I'm asking in a more generalized form, what practical use has anyone found for this method?)


Well, there's nothing like having a minimal reproducible example to play with, so here is one.

Make a plain vanilla iOS project with its template ViewController, and add this code to the ViewController.

I'll take it piece by piece. First, we have a struct that will serve as our item identifier. The UUID is the unique part, so equatability and hashability depend upon it alone:

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

Next, the (fake) table view and the diffable data source:

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

So there is just one UniBool in our diffable data source and its bool is true. So now set up a button to call this action method which tries to toggle the bool value by using 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)
        }
    }
}

So here's the thing. I said to reloadItems with an item whose UUID is a match, but whose bool is toggled: "this object's isON is false". But when I ask the snapshot, okay, what have you got? it tells me that its sole item identifier's bool is still true.

And that is what I'm asking about. If the snapshot is not going to pick up the new value of bool, what is reloadItems for in the first place?

Obviously I could just substitute a different UniBool, i.e. one with a different UUID. But then I cannot call reloadItems; we crash because that UniBool is not already in the data. I can work around that by calling insert followed by remove, and that is exactly how I do work around it.

But my question is: so what is reloadItems for, if not for this very thing?

解决方案

(I've filed a bug on the behavior demonstrated in the question, because I don't think it's good behavior. But, as things stand, I think I can provide a guess as to what the idea is intended to be.)


When you tell a snapshot to reload a certain item, it does not read in the data of the item you supply! It simply looks at the item, as a way of identifying what item, already in the data source, you are asking to reload.

(So, if the item you supply is Equatable to but not 100% identical to the item already in the data source, the "difference" between the item you supply and the item already in the data source will not matter at all; the data source will never be told that anything is different.)

When you then apply that snapshot to the data source, the data source tells the table view to reload the corresponding cell. This results in the data source's cell provider function being called again.

OK, so the data source's cell provider function is called, with the usual three parameters — the table view, the index path, and the data from the data source. But we've just said that the data from the data source has not changed. So what is the point of reloading at all?

The answer is, apparently, that the cell provider function is expected to look elsewhere to get (at least some of) the new data to be displayed in the newly dequeued cell. You are expected to have some sort of "backing store" that the cell provider looks at. For example, you might be maintaining a dictionary where the key is the cell identifier type and the value is the extra information that might be reloaded.

This must be legal, because by definition the cell identifier type is Hashable and can therefore serve as a dictionary key, and moreover the cell identifiers must be unique within the data, or the data source would reject the data (by crashing). And the lookup will be instant, because this is a dictionary.


Here's a complete working example you can just copy and paste right into a project. The table portrays three names along with a star that the user can tap to make star be filled or empty, indicating favorite or not-favorite. The names are stored in the diffable data source, but the favorite status is stored in the external backing store.

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

这篇关于NSDiffableDataSourceSnapshot`reloadItems`是做什么用的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆