Differ 生成 Collection
实例之间的差异 (包括字符串!)。
它使用 快速算法 (O((N+M)*D))
来实现这一点。
ExtendedDiff
时)Patch
) 的任意排序UITableView
和 UICollectionView
,以及 AppKit 中的 NSTableView
和 NSCollectionView
的实用工具NestedDiff
)计算差异不仅仅是为了轻松执行表格视图动画!
无论你在哪里有代码将 added
/removed
/moved
回调从你的模型传播到你的用户界面,你都应该考虑使用一个可以计算差异的库。与重新加载所有数据相比,动画处理小批量更改通常会更快,并提供更灵敏的体验。
计算和处理差异也有助于你在数据和用户界面之间建立清晰的分离,并有望提供更声明式的方法:你的模型执行状态转换,然后你的 UI 代码根据计算出的状态差异执行适当的操作。
让我们考虑一个使用补丁将字符串 "a"
转换为 "b"
的简单示例。以下步骤描述了在这些状态之间移动所需的补丁
更改 | 结果 |
---|---|
删除索引 0 处的项目 | "" |
在索引 0 处插入 b |
"b" |
如果我们想以不同的顺序执行这些操作,简单地重新排序现有的补丁将不起作用
更改 | 结果 |
---|---|
在索引 0 处插入 b |
"ba" |
删除索引 0 处的项目 | "a" |
...糟糕!
为了获得正确的结果,我们需要移动插入和删除的顺序,以便得到这个
更改 | 结果 |
---|---|
在索引 1 处插入 b |
"ab" |
删除索引 0 处的项目 | "b" |
为了缓解这个问题,有两种类型的输出
ExtendedDiff
) 的序列,其中删除指向源中要删除的项目的位置,插入指向输出中的项目。Differ 只生成一个 Diff
。Diff
的,但它可以是任意排序的。在实践中,这意味着将字符串 1234
转换为 1
的差异可以描述为以下步骤集
DELETE 1
DELETE 2
DELETE 3
描述相同更改的补丁将是
DELETE 1
DELETE 1
DELETE 1
但是,如果我们决定对其进行排序,以便首先处理删除和较高索引,我们将得到此补丁
DELETE 3
DELETE 2
DELETE 1
以下内容将自动动画处理删除、插入和移动
tableView.animateRowChanges(oldData: old, newData: new)
collectionView.animateItemChanges(oldData: old, newData: new, updateData: { self.dataSource = new })
它也可以与 section 一起使用!
tableView.animateRowAndSectionChanges(oldData: old, newData: new)
collectionView.animateItemAndSectionChanges(oldData: old, newData: new, updateData: { self.dataSource = new })
你还可以单独计算 diff
并在以后使用它
// Generate the difference first
let diff = dataSource.extendedDiff(newDataSource)
// This will apply changes to dataSource.
let dataSourceUpdate = { self.dataSource = newDataSource }
// ...
tableView.apply(diff)
collectionView.apply(diff, updateData: dataSourceUpdate)
请参阅 包含的示例 以获取工作示例。
自版本 2.0.0
起,现在有一个 updateData
闭包,它会在适当的时间通知你更新 UICollectionView
的 dataSource
。此添加指的是 UICollectionView 的 performbatchUpdates
如果在你调用此方法之前,集合视图的布局不是最新的,则可能会发生重新加载。为避免出现问题,你应在更新块内更新数据模型,或确保在调用
performBatchUpdates(_:completion:)
之前更新布局。
因此,建议在 updateData
闭包内更新你的 dataSource
,以避免在动画期间可能发生的崩溃。
当你想确定将一个集合转换为另一个集合的步骤时 (例如,你想根据模型中的更改来动画你的用户界面),你可以执行以下操作
let from: T
let to: T
// patch() only includes insertions and deletions
let patch: [Patch<T.Iterator.Element>] = patch(from: from, to: to)
// extendedPatch() includes insertions, deletions and moves
let patch: [ExtendedPatch<T.Iterator.Element>] = extendedPatch(from: from, to: to)
当你需要对排序进行额外控制时,你可以使用以下方法
let insertionsFirst = { element1, element2 -> Bool in
switch (element1, element2) {
case (.insert(let at1), .insert(let at2)):
return at1 < at2
case (.insert, .delete):
return true
case (.delete, .insert):
return false
case (.delete(let at1), .delete(let at2)):
return at1 < at2
default: fatalError() // Unreachable
}
}
// Results in a list of patches with insertions preceding deletions
let patch = patch(from: from, to: to, sort: insertionsFirst)
一个高级示例:你想要先计算差异,然后生成补丁。在某些情况下,这可以提高性能。
D
是差异的长度
O(D^2)
时间。O(D)
。// Generate the difference first
let diff = from.diff(to)
// Now generate the list of patches utilising the diff we've just calculated
let patch = diff.patch(from: from, to: to)
如果你想了解更多关于此库的工作原理,Graph.playground
是一个很好的起点。
Differ 速度很快。许多其他 Swift 差异库使用简单的 O(n*m)
算法,该算法分配一个二维数组,然后遍历每个元素。这可能会占用大量内存。
在以下基准测试中,你应该看到两种算法之间计算时间的数量级差异。
每个测量值是在 iPhone 6 上运行 10 次计算差异的平均时间 (秒)。
差异 (Diff) | Dwifft | |
---|---|---|
相同 | 0.0213 | 52.3642 |
已创建 | 0.0188 | 0.0033 |
已删除 | 0.0184 | 0.0050 |
差异 | 0.1320 | 63.4084 |
你可以通过 查看 Diff Performance Suite 自己运行这些基准测试。
综上所述,Diff 使用的算法最适用于集合之间差异较小的情况。但是,即使对于较大的差异,此库仍然可能比那些使用简单的 O(n*m)
算法的库更快。如果你需要在大差异集合之间获得更好的性能,请考虑实施更合适的方法,例如 Hunt & Szymanski 算法 和/或 Hirschberg 算法。
Differ 需要至少 Swift 5.4 或 Xcode 12.5 才能编译。
你可以使用 Carthage、CocoaPods、Swift Package Manager 或作为 Xcode 子项目将 Differ 添加到你的项目中。
github "tonyarnold/Differ"
pod 'Differ'
Differ 是 Wojtek Czekalski 的 Diff.swift 的修改后的分支 - Wojtek 值得获得原始实现的所有功劳,我只是它现在的保管人。
请 在此存储库中的此分支中提交问题,而不是在 Wojtek 的原始存储库中提交。