swift NSDiffableDataSourceSnapshot '重新加载项'的作用是什么?

qco9c6ql  于 2023-02-28  发布在  Swift
关注(0)|答案(5)|浏览(177)

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

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

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

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

你可能认为第二点的答案是:当然,项目标识符对象的某些其他方面可能不属于它的等同性,但确实反映到了单元格接口中,但我发现这不是真的;调用reloadItems后,表格视图 * 不 * 反映更改。
所以当我想改变一个项目时,我最终对快照所做的是在要替换的项目之后输入一个insert,然后是原始项目的delete。没有snapshot 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
    }
}

接下来,(伪)表视图和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,它的booltrue,所以现在设置一个按钮来调用这个action方法,该方法尝试使用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)
        }
    }
}

事情是这样的,我对reloadItems说,它的UUID是匹配的,但是它的bool是切换的:"this object's isON is false"。但是当我询问快照时,好吧,你得到了什么?它告诉我它唯一的项标识符bool * 仍然是true *。
这就是我要问的问题,如果快照不能获得bool的新值,那么reloadItems首先是用来做什么的?
显然,我可以用一个不同的UniBool来代替,也就是用一个不同的UUID来代替,但是这样我就不能调用reloadItems了;我们崩溃是因为UniBool不在数据中,我可以通过调用insertremove来解决这个问题,这就是我解决这个问题的方法。
但我的问题是:那么reloadItems如果不是为了这个东西的话,又是为了什么呢?

ax6ht2ek

ax6ht2ek1#

(我对问题中的行为提出了一个错误,因为我认为这不是一个好的行为,但是,就目前的情况来看,我想我可以提供一个猜测,说明这个想法的意图是什么。)
当你告诉一个快照reload一个特定的条目时,它并不读取你提供的条目的数据!它只是简单地查看条目,作为一种识别数据源中已经存在的条目的方法,你要求重新加载。
(So,如果您提供的项目与数据源中已有的项目相等但不是100%相同,则您提供的项目与数据源中已有的项目之间的“差异”将无关紧要;数据源将永远不会被告知有任何不同。)
当您将快照apply到数据源时,数据源会告诉表视图重新加载相应的单元格,这会导致数据源的 *cell provider函数 * 被再次调用。
好了,调用数据源的单元格提供器函数,使用三个常用参数:表视图、索引路径和来自数据源的数据,但是我们刚刚说过来自数据源的数据 * 没有改变 *,那么重新加载的意义何在?
答案显然是,单元格提供程序函数应该从 * 别处 * 获取(至少部分)要在新出队的单元格中显示的新数据。您应该具有某种类型的“后备存储”,单元格提供程序会查看它。例如,您可能正在维护一个字典,其中键是单元格标识符类型,值是可能要重新加载的额外信息。
这必须是法律的的,因为根据定义,单元格标识符类型是哈希的,因此可以用作字典键,此外,单元格标识符在数据中必须是唯一的,否则数据源将拒绝数据(通过崩溃),并且查找将是即时的,因为这是一个字典。
下面是一个完整的工作示例,您可以直接复制并粘贴到项目中。表格描绘了三个名称沿着一个星星,用户可以点击星号使其填充或空白,以指示喜爱或不喜爱。名称存储在可区分数据源中,但喜爱状态存储在外部后备存储中。

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

gstyhher2#

基于你的新示例代码,我同意,它看起来像一个bug。当你添加一个reloadItems到一个快照,它正确地触发数据源闭包请求一个更新的单元格,但传递到闭包的IdentifierType项是原始的,而不是reloadItems调用提供的新值。
如果我将UniBool结构体更改为类,使其成为引用而不是值类型,那么事情就会按预期运行(因为现在有一个UniBool的单个示例,而不是一个具有相同标识符的新示例)。
目前似乎有几种可能的变通办法:
1.对IdentifierType使用引用而不是值类型
1.使用一个额外的后备存储(如数组),并通过数据源闭包中的indexPath访问它。
我不认为这两个是理想的。
有趣的是,在我将UniBool更改为类之后,我尝试创建一个UniBool的新示例,该示例具有与现有示例相同的uuid,并重新加载它;代码崩溃,出现异常,指出 * 为重新加载指定的项目标识符无效 *;我觉得这不太对只有hashValue是重要的,而不是实际的对象引用。原始对象和新对象都有相同的hashValue,并且==返回true

原始答案

reloadItems可以工作,但有两点很重要:
1.必须从数据源的当前snapshot开始,并在其上调用reloadItems
1.除了identifier * 之外,您不能依赖传递给CellProvider闭包 * 的item-它不代表来自您的备份模型(数组)的最新数据。
第2点意味着您需要使用提供的indexPathitem.id从模型中获取更新的对象。
我创建了一个简单的example,它在表行中显示当前时间;下面是数据源结构:

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

nhhxz33t3#

我也提出了同样的问题,但没有意识到。我首先将我的模型转换为类,然后在调用'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)
}
8e2ybdfx

8e2ybdfx4#

我发现(通过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)

这些对我很有效。

hrysbysz

hrysbysz5#

在我的示例中,添加reloadedItem检查:

if #available(iOS 15.0, *) {
    if !snapshot.reloadedItemIdentifiers.contains(YourItem) {
        snapshot.reloadItems([YourItem])
        self.dataSource.apply(snapshot, animatingDifferences: false)
    }
} else {
    snapshot.reloadItems([YourItem])
    self.dataSource.apply(snapshot, animatingDifferences: false)
}

相关问题