PHDiff

是一个非常高效的 diff 算法,用 Swift 实现,在时间和空间上都具有线性复杂度。

基于 Paul Heckel 的论文:一种隔离文件差异的技术

给定两个不同的数组 A 和 B,A 需要进行哪些步骤才能变成 B?

PHDiff 可以通过计算所需的插入、删除、移动和更新来回答这个问题!

PHDiff 还可以为 UITableView 和 UICollectionView 的更改提供批量更新,由于它速度极快,因此可以直接在主队列中使用。

要求

安装

CocoaPods

要使用 CocoaPods 将 PHDiff 集成到您的 Xcode 项目中,请在您的 Podfile 中指定它

use_frameworks!

target '<Your Target Name>' do
    pod 'PHDiff', '~> 1.2'
end

Carthage

要使用 Carthage 将 PHDiff 集成到您的 Xcode 项目中,请在您的 Cartfile 中指定它

github "andre-alves/PHDiff" ~> 1.2

运行 carthage update 来构建框架,并将构建的 PHDiff.framework 拖到您的 Xcode 项目中。

Swift Package Manager

Swift Package Manager 是一种用于自动化 Swift 代码分发的工具,并已集成到 swift 编译器中。

一旦您设置好 Swift 包,添加 PHDiff 作为依赖项就像将其添加到 Package.swiftdependencies 值一样简单。

dependencies: [
    .package(url: "https://github.com/andre-alves/PHDiff.git", .upToNextMajor(from: "1.2.0"))
]

手动

只需将 Sources 文件夹中的 .swift 文件复制到您的项目中。

用法

根据安装方法,您可能需要在使用的文件中导入 PHDiff

import PHDiff

PHDiff 提供了两个方法:PHDiff.sortedSteps(fromArray:toArray:)PHDiff.steps(fromArray:toArray:),它们都返回一个 DiffSteps 数组

public enum DiffStep<T> {
    case insert(value: T, index: Int)
    case delete(value: T, index: Int)
    case move(value: T, fromIndex: Int, toIndex: Int)
    case update(value: T, index: Int)
}

PHDiff.sortedSteps(fromArray:toArray:) 以排序的方式计算插入、删除和更新,这些操作可以应用于第一个数组以将其转换为第二个数组。

let a = ["a", "b", "c", "d"]
let b = ["e", "a", "d"]
let steps = PHDiff.sortedSteps(fromArray: a, toArray: b)
print(steps)
//[Delete c at index: 2, Delete b at index: 1, Insert e at index: 0]
print(a.apply(steps: steps))
//["e", "a", "d"]

PHDiff.steps(fromArray:toArray:) 计算插入、删除、移动和更新,仅用于批量操作(例如:UITableView 和 UICollectionView 批量更新)。

    private func updateTableView(newColors: [DemoColor]) {
        let steps = PHDiff.steps(fromArray: self.colors, toArray: newColors)

        if steps.count > 0 {
            tableView.beginUpdates()
            self.colors = newColors // update your model here

            var insertions: [IndexPath] = []
            var deletions: [IndexPath] = []
            var reloads: [IndexPath] = []

            steps.forEach { step in
                switch step {
                case let .insert(_, index):
                    insertions.append(IndexPath(row: index, section: 0))
                case let .delete(_, index):
                    deletions.append(IndexPath(row: index, section: 0))
                case let .move(_, fromIndex, toIndex):
                    deletions.append(IndexPath(row: fromIndex, section: 0))
                    insertions.append(IndexPath(row: toIndex, section: 0))
                case let .update(_, index):
                    reloads.append(IndexPath(row: index, section: 0))
                }
            }

            tableView.insertRows(at: insertions, with: .automatic)
            tableView.deleteRows(at: deletions, with: .automatic)
            tableView.reloadRows(at: reloads, with: .automatic)
            
            tableView.endUpdates()
        }
    }

重要提示

为了对您的模型进行差异比较,它们需要符合 Diffable 协议

Diffable 协议

Diffable 通过提供一个 diffIdentifier 扩展了 Equatable 协议。它可以是 String、Int 或任何符合 Hashable 协议的内容。您可以将其视为表示您的对象的唯一键。

struct DemoColor: Diffable {
    let name: String
    let r: Float
    let g: Float
    let b: Float
    
    var diffIdentifier: String {
        return name
    }
}

func ==(lhs: DemoColor, rhs: DemoColor) -> Bool {
    return lhs.name == rhs.name && lhs.r == rhs.r && lhs.b == rhs.b && lhs.g == rhs.g
}

注意:如果您的模型符合 Hashable,则不需要实现 diffIdentifier。

性能

差异比较两个长度均为 1000 的随机生成的数组

Performance Test

在 MacBook Pro (Retina, 13-inch, Mid 2014) - 2.6 GHz Intel Core i5 上测试。

致谢

Dwifft - 我使用了相同的枚举名称 'DiffStep'。

IGListKit - 我使用了协议 Diffable 的概念和一个小的优化来避免不必要的步骤。

许可

PHDiff 根据 MIT 许可证发布。 有关详细信息,请参见 LICENSE。