Differ

Continuous Integration

Differ 生成 Collection 实例之间的差异 (包括字符串!)。

它使用 快速算法 (O((N+M)*D)) 来实现这一点。

特性

我为什么需要它?

计算差异不仅仅是为了轻松执行表格视图动画!

无论你在哪里有代码将 added/removed/moved 回调从你的模型传播到你的用户界面,你都应该考虑使用一个可以计算差异的库。与重新加载所有数据相比,动画处理小批量更改通常会更快,并提供更灵敏的体验。

计算和处理差异也有助于你在数据和用户界面之间建立清晰的分离,并有望提供更声明式的方法:你的模型执行状态转换,然后你的 UI 代码根据计算出的状态差异执行适当的操作。

差异、补丁和排序

让我们考虑一个使用补丁将字符串 "a" 转换为 "b" 的简单示例。以下步骤描述了在这些状态之间移动所需的补丁

更改 结果
删除索引 0 处的项目 ""
在索引 0 处插入 b "b"

如果我们想以不同的顺序执行这些操作,简单地重新排序现有的补丁将不起作用

更改 结果
在索引 0 处插入 b "ba"
删除索引 0 处的项目 "a"

...糟糕!

为了获得正确的结果,我们需要移动插入和删除的顺序,以便得到这个

更改 结果
在索引 1 处插入 b "ab"
删除索引 0 处的项目 "b"

解决方案

为了缓解这个问题,有两种类型的输出

实际排序

在实践中,这意味着将字符串 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)

请参阅 包含的示例 以获取工作示例。

关于 updateData 的注意事项

自版本 2.0.0 起,现在有一个 updateData 闭包,它会在适当的时间通知你更新 UICollectionViewdataSource。此添加指的是 UICollectionView 的 performbatchUpdates

如果在你调用此方法之前,集合视图的布局不是最新的,则可能会发生重新加载。为避免出现问题,你应在更新块内更新数据模型,或确保在调用 performBatchUpdates(_:completion:) 之前更新布局。

因此,建议updateData 闭包内更新你的 dataSource,以避免在动画期间可能发生的崩溃。

使用 Patch 和 Diff

当你想确定将一个集合转换为另一个集合的步骤时 (例如,你想根据模型中的更改来动画你的用户界面),你可以执行以下操作

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 是差异的长度

// 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 添加到你的项目中。

Carthage

github "tonyarnold/Differ"

CocoaPods

pod 'Differ'

致谢

Differ 是 Wojtek CzekalskiDiff.swift 的修改后的分支 - Wojtek 值得获得原始实现的所有功劳,我只是它现在的保管人。

在此存储库中的此分支中提交问题,而不是在 Wojtek 的原始存储库中提交。