DifferenceKit地址
一个快速灵活的Swift集合O(n)差分算法框架。该算法在Paul Heckel算法的基础上进行了优化。
特征
💡 Swift集合优化的最快O(n)查分算法
💡 计算UIKit、AppKit和Texture中列表UI批量更新的差异
💡 即使包含重复项,也支持线性和分段收集
💡 支持动画UI批量更新的各种差异
算法
这是为Carbon开发的一个困难的算法,单独工作。该算法在Paul Heckel算法的基础上进行了优化。另见他1978年发表的论文“一种隔离文件之间差异的技术”。它允许在线性时间O(n)内计算所有类型的微分。RxDataSources和IGListKit也是基于他的算法实现的。
但是,在UITableView、UICollectionView等的performBatchUpdates中,同时应用时会出现差异组合,从而导致崩溃。
为了解决这个问题,DifferenceKit采取了一种方法,在最短的阶段拆分差异集,可以在没有崩溃的情况下执行批量更新。
实现方式
基础使用
要获取diff的元素的类型必须符合Differentiable协议。
differenceIdentifier的类型是通用关联类型:
struct User: Differentiable {
let id: Int
let name: String
var differenceIdentifier: Int {
return id
}
func isContentEqual(to source: User) -> Bool {
return name == source.name
}
}
在上面定义的情况下,id唯一地标识元素,并通过比较源和目标中元素名称的相等性来了解更新的用户。
对于符合Equatable或Hashable的类型,有Differentiable的默认实现:
// If `Self` conforming to `Hashable`.
var differenceIdentifier: Self {
return self
}
// If `Self` conforming to `Equatable`.
func isContentEqual(to source: Self) -> Bool {
return self == source
}
因此,您可以简化
extension String: Differentiable {}
通过从符合Differentiable的两个元素集合创建StagedChangeset来计算diff:
let source = [
User(id: 0, name: "Vincent"),
User(id: 1, name: "Jules")
]
let target = [
User(id: 1, name: "Jules"),
User(id: 0, name: "Vincent"),
User(id: 2, name: "Butch")
]
let changeset = StagedChangeset(source: source, target: target)
如果要在集合中包括多个符合Differentiable的类型,请使用AnyDifferentible:
let source = [
AnyDifferentiable("A"),
AnyDifferentiable(User(id: 0, name: "Vincent"))
]
在分段集合的情况下,节本身必须具有唯一的标识符,并且能够比较是否有更新。
因此,每个节都必须符合DifferentiableSection协议,但在大多数情况下,您可以使用符合它的通用类型的ArraySection。
ArraySection需要一个符合Differentiable的模型,以区别于其他部分:
enum Model: Differentiable {
case a, b, c
}
let source: [ArraySection<Model, String>] = [
ArraySection(model: .a, elements: ["A", "B"]),
ArraySection(model: .b, elements: ["C"])
]
let target: [ArraySection<Model, String>] = [
ArraySection(model: .c, elements: ["D", "E"]),
ArraySection(model: .a, elements: ["A"]),
ArraySection(model: .b, elements: ["B", "C"])
]
let changeset = StagedChangeset(source: source, target: target)
您可以使用创建的StagedChangeset执行UITableView和UICollectionView的艰难批量更新。
⚠️ 不要忘记同步更新数据源引用的数据,以及在setData闭包中传递的数据。diff是分阶段应用的,否则势必会造成崩溃:
tableView.reload(using: changeset, with: .fade) { data in
dataSource.data = data
}
使用过多差异的批量更新可能会对性能产生不利影响。
通过中断关闭返回true,然后返回到reloadData:
collectionView.reload(using: changeset, interrupt: { $0.changeCount > 100 }) { data in
dataSource.data = data
}
UICollectionView扩展
import Foundation
import DifferenceKit
import UIKit
public extension UICollectionView {
func reload<C>(
using stagedChangeset: StagedChangeset<C>,
interrupt: ((Changeset<C>) -> Bool)? = nil,
onInterruptedReload: (() -> Void)? = nil,
completion: ((Bool) -> Void)? = nil,
setData: (C) -> Void
) {
if case .none = window, let data = stagedChangeset.last?.data {
setData(data)
if let onInterruptedReload {
onInterruptedReload()
} else {
reloadData()
}
completion?(false)
return
}
let dispatchGroup: DispatchGroup? = completion != nil
? DispatchGroup()
: nil
let completionHandler: ((Bool) -> Void)? = completion != nil
? { _ in
dispatchGroup!.leave()
}
: nil
for changeset in stagedChangeset {
if let interrupt, interrupt(changeset), let data = stagedChangeset.last?.data {
setData(data)
if let onInterruptedReload {
onInterruptedReload()
} else {
reloadData()
}
completion?(false)
return
}
performBatchUpdates({
setData(changeset.data)
dispatchGroup?.enter()
if !changeset.sectionDeleted.isEmpty {
deleteSections(IndexSet(changeset.sectionDeleted))
}
if !changeset.sectionInserted.isEmpty {
insertSections(IndexSet(changeset.sectionInserted))
}
if !changeset.sectionUpdated.isEmpty {
reloadSections(IndexSet(changeset.sectionUpdated))
}
for (source, target) in changeset.sectionMoved {
moveSection(source, toSection: target)
}
if !changeset.elementDeleted.isEmpty {
deleteItems(at: changeset.elementDeleted.map {
IndexPath(item: $0.element, section: $0.section)
})
}
if !changeset.elementInserted.isEmpty {
insertItems(at: changeset.elementInserted.map {
IndexPath(item: $0.element, section: $0.section)
})
}
if !changeset.elementUpdated.isEmpty {
reloadItems(at: changeset.elementUpdated.map {
IndexPath(item: $0.element, section: $0.section)
})
}
for (source, target) in changeset.elementMoved {
moveItem(at: IndexPath(item: source.element, section: source.section), to: IndexPath(item: target.element, section: target.section))
}
}, completion: completionHandler)
}
dispatchGroup?.notify(queue: .main) {
completion!(true)
}
}
}