UITableViewDiffableDataSource可以检测到项目的更改吗?

11

我在使用带有 diffable 数据源的 UITableView 时遇到了问题。在我的应用程序中,当用户修改一个项目时,它可能会更改在同一表视图中显示的另一个项目。问题是,在我创建和应用包含两个项目新值的新快照后,间接更改的项目的 UI 没有更新。

起初,我认为 diffable 数据源能够检测不同快照中项目的值更改。例如,它可能是这样工作的:如果它发现两个快照都包含相同的项目(即,在两个快照中的项目具有相同的哈希值),则比较它们的值并更新表视图中该项目的行,如果该值更改。然而,后来我意识到它可能不是这样工作的,因为 diffable 数据源没有定义任何 API 来获取和比较项目值(我最初的想法是它使用description 计算属性和 == 操作,但现在我相信这不是真的)。

因此,我目前的理解是 diffable 数据源使用项目哈希来检测 项目订单更改 (即插入新项目,旧项目仍存在等),而不是 项目值更改 (即旧项目仍存在但其值已更改)。如果这种理解是正确的,那么就会引出这个问题:如何使用 diffable 数据源实现以下场景?

  • 一个项目有若干属性。其中一种属性(我们称之为属性A)在UI中显示,但不用于生成哈希值。
  • 该项目存在于旧和新的快照中,但其属性A发生了变化。所以需要更新其UI。

在旧的UITableView API中,可以通过调用reloadRows()reloadData()来实现这一点。但是如何使用可差异数据源实现呢?

更新:

经过时间的实验和解决问题,我认为以上问题的理解是错误的。请看下面我的答案。我相信那说明了可差分数据源的工作方式。我希望它能对那些会遇到同样困惑的人有所帮助。如果你有不同的看法,请留下你的答案,我很乐意被证明是错的。真的。

3个回答

9

经过一天的尝试,我相信我已经理解了可差异数据源如何工作,并基于这一理解解决了我的问题(结果证明我的原始想法几乎是正确的)。

可差异数据源使用项目哈希来标识项。 对于在旧和新快照中都存在的相同项目,可差异数据源通过使用“==”运算符检查其旧值和新值来检查该项是否发生更改。

一旦理解了它,看起来就很明显和简单。 但它是如此基础,以至于我无法理解为什么没有在任何地方明确提到。

因此,回答我的最初问题,是的,可差异数据源可以检测项目值的更改。 也就是说,当项目值是引用类型和/或行中显示的文本是该对象引用的对象的属性时(例如Core Data中的关系等),情况就变得棘手了。

另一个注意点。 生成项目哈希时是否使用整个项目结构或仅其中的一部分并不重要,只要它能识别该项即可。 我更喜欢仅使用实际标识其的基本部分。


1
“但这是如此基础的东西,我不明白为什么它没有在任何地方明确提到。” 它已经在各处明确提到了。哈希性是可差异数据源工作的方式。哈希性意味着可比较性,意味着 == - matt
2
@matt 到处都提到 Hashable 在 Swift 中要求 Equatable。但是在 diffable data source 中没有任何地方提到如何使用 ==。在 WWDC 2019 Session 220 视频的示例代码中,== 函数的定义与 hash(into:) 函数基本相同。在我看来,这非常具有误导性。除非我完全误解了它,否则 diffable data source 实际上非常好地处理了两个具有相同哈希值但不相等的项的情况。我认为这是 diffable data source 设计的一部分,演讲者们真的应该明确提到这一点。 - rayx
3
换句话说,可 diffable 的数据源中的所有部分标识符或行标识符都必须是唯一的,就像 Set 中的所有元素一样。唯一意味着它们之间互不相等,即使用 == 运算符比较时结果为真。这是数据源识别对象的关键方法,这也是它们被称为标识符的原因!如果一个对象在第 1 行,而一个等于它的对象现在在第 3 行,则它们必须是同一个对象。哈希性只是一种工具,可以使此判断在不需要每次检查每个值的情况下更快速地进行。 - matt
1
您的描述是正确的。还有一个问题。如果您使用哈希来标识对象并使用==测试某些值是否更改,则可区分数据源将删除和插入行。如果您想让可区分数据源仅更新行,则必须在新快照上设置reloadItems。 编辑:我认为这是NSFetchedResultsController中的一个错误。NSFetchedResultsController应该使用reloadItems,但它没有。这里是手动生成reloadItems的解决方案。https://dev59.com/G1MH5IYBdhLWcg3wvyE4#65293427 - Wilko X
1
我相信这样的理解:“Diffable数据源使用项哈希来标识项”。因为两个具有不同标识的项最终可能具有相同的哈希值。实际上,我认为iOS的Diffable数据源确实存在严重缺陷,特别是当一个项目同时具有内容更改和位置更改时。当我们与Android交叉检查他们如何实现Diffable概念时,这个缺陷更加明显-https://developer.apple.com/forums/thread/653647。 - Cheok Yan Cheng
显示剩余16条评论

3
我有同样的问题。经过一些研究,我认为Hashable不是处理更新功能的方法。你可以从这里的文档中看到:https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/updating_collection_views_using_diffable_data_sources
它有两种加载可区分数据源的方式:使用标识符加载可区分数据源使用轻量级数据结构填充快照
虽然苹果推荐第一种方法。在此方法中,我们使用snapshot.reconfigureItems来更新现有项。
    struct Recipe: Identifiable, Codable {
        var id: Int
        var title: String
        
        // and many other properties
        xxxxx
    }

    // Get the diffable data source's current snapshot.
    var snapshot = recipeListDataSource.snapshot()
    // Update the recipe's data displayed in the collection view.
    snapshot.reconfigureItems([recipeId])
    recipeListDataSource.apply(snapshot, animatingDifferences: true). 

重点是我们在快照中使用Recipe.ID而不是Recipe,类型为NSDiffableDataSourceSnapshot<RecipeListSection,Recipe.ID>

对于我们都在使用的第二种方法,在快照中放置可哈希模型,这是苹果关于它的说法:

这种方法的缺点是diffable数据源无法跟踪身份。每次现有项更改时,diffable数据源将更改视为旧项的删除和新项的插入。因此,集合视图失去了与该项相关的重要状态。例如,选定的项目变为未选定状态,因为从diffable数据源的角度来看,应用程序删除了该项并添加了一个新项以取代它。

此外,如果在应用快照时animatingDifferences为true,则每个更改都需要动画化旧单元格并动画化新单元格的过程,这可能会损害性能并导致UI状态(包括单元格内的动画)丢失。

此策略还排除了使用reconfigureItems(:)或reloadItems(:)方法在使用数据结构填充快照时的可能性,因为这些方法需要使用正确的标识符。更新现有项目的唯一机制是应用包含新数据结构的新快照,这会导致diffable数据源对每个更改的项目执行删除和插入操作。

将数据结构直接存储到可差异数据源和快照中并不是许多实际用例的强大解决方案,因为数据源失去了跟踪身份的能力。仅在像此示例中的侧边栏项目这样的简单用例或项的身份不重要时使用此方法。对于所有其他用例,或者在怀疑使用哪种方法时,请使用适当的标识符填充diffable数据源和快照。


1
谢谢提供指针。当我发出问题时(时间飞逝!),文章还没有发布。我认为它确认了我自己答案中解释的要点:a)可差异数据源使用项哈希值作为其标识,b)通过比较其值来检测项更改。第一点是最重要的(它是第二点的基础),在我发布问题和答案时,网络上没有任何地方提到它。 - rayx
2
个人认为使用哈希值作为项目标识符(悄悄地)是一个令人困惑的设计决策(这种方法在其他地方使用吗?我想不出来)。更好的方法是让 SectionIdentifierTypeItemIdentifierType 符合 Identifiable 协议。这是 SwiftUI 中广泛使用的方法。 - rayx

1

我对你最后一句话有点困惑:你写道我的项目是一个具有引用类型相关值的枚举,但在你上面的例子中使用了struct Book,这是一个值类型。无论如何,对于任何情况,都必须记住以下几点:

哈希处理与“对象”身份有关。它只是一种提高身份比较、折叠等方面的快捷方式。

如果您提供自定义的哈希实现,则两个对象ab必须表现出a == b意味着hash(a) == hash(b)的方式(反过来也几乎总是正确的,但可能存在碰撞-特别是在使用弱哈希算法时-当不是这种情况时)。

因此,如果您只对titleauthor进行哈希处理,则必须以一种使其仅比较titleauthor的方式实现比较运算符。然后,如果notes更改,则数据源和任何人都不会检测到身份更改。

UITableViewDiffableDataSource是一种简化视图和数据源之间插入/删除语句同步的方式。如果你曾经遇到过以下错误:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (3) must be equal to the number of sections contained in the table view before the update (3), plus or minus the number of sections inserted or deleted (0 inserted, 2 deleted).'

那么使用可差异性数据源会很有帮助。希望这能对你有所帮助。

嗨@@AndreasOetjen,感谢您的回答,对于混淆我感到抱歉(它们是两个不同的示例。我应该使用不同的名称)。恐怕我可能没有清楚表达我的困惑。让我这样说吧。在旧的UITableViewDataSource API中,如果更改数据源中的项目,则可以调用UITableView.reloadRows()。在可区分数据源API中有什么等效的方法可以实现呢?新API是否支持具有特定标识符的项目的值发生更改的概念?(我无法从新API中看到这一点)。 - rayx
哦,等等……也许可区分的数据源并不真正关心项目的值,对吧?它似乎只关心项目顺序的变化,这就是为什么它需要项目标识符的原因。至于项目的值,它只在cellProvider闭包中使用用户提供的代码。在我的应用程序中,当用户编辑表视图中的一个项目时,可能会更改表视图中的另一个项目。问题在于,当该另一个项目在数据层中被更改时,其新值没有在UI层中更新。看起来这可能是除了可区分的数据源之外的其他问题引起的? - rayx
通常情况下,您不会调用 reloadData,而是使用 NSDiffableDataSourceSnapshotdataSource.apply 进行操作。可变数据源关心部分和行的更改、插入和删除。这可能值得一读:https://wwdcbysundell.com/2019/diffable-data-sources-first-look/ 和 https://medium.com/@alfianlosari/using-diffable-data-source-ios-13-api-in-uitableview-47343c2332be - Andreas Oetjen
谢谢指针,我知道如何使用它。我刚刚更新了我的问题,以使其更加清晰。谢谢。 - rayx
如果_identity_没有改变(因此没有添加/移动/删除),您应该能够使用重新加载给定索引路径的单元格(reloadRows(at:with:))来处理,因此您不需要将注释添加到哈希算法中。 - Andreas Oetjen
1
但这不是违背了可区分数据源的目的吗?(在我的应用程序中,很难确定哪些项目是作为副作用而更改的)。此外,我刚刚尝试了描述在我的问题中的方法(将“属性A”添加到哈希中),但它没有起作用。我检查了哈希值,在两个快照中是不同的,但其UI没有更新(我没有使用示例结构。我的应用程序中的一个枚举包含NSManagedObject作为其关联值)。我仍在努力弄清楚出了什么问题。唉。如果我无法确定根本原因,我可能必须采取reloadData()的方法。 - rayx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接