A fast and flexible O(n) difference algorithm framework for Swift collection.
The algorithm is optimized based on the Paul Heckel's algorithm.
β Automate to calculate operations for batch-updates of UITableView, UICollectionView, NSTableView and NSCollectionView
β O(n) difference algorithm optimized for performance in Swift
β Supports both linear and sectioned collection
β Supports calculating differences with best effort even if elements or section contains duplicates
β Supports all operations for animated UI batch-updates including section reloads
Sample app is imitated from DataSources πΎ
The algorithm used in DifferenceKit is optimized based on the Paul Heckel's algorithm.
See also his paper "A technique for isolating differences between files" released in 1978.
RxDataSources and IGListKit are also implemented based on his algorithm.
This allows all types of differences to be computed in linear time O(n).
However, in performBatchUpdates
of UITableView
and UICollectionView
, there are combinations of operations that cause crash when applied simultaneously.
To solve this problem, DifferenceKit
takes an approach of split the set of differences at the minimal stages that can be perform batch-updates with no crashes.
Implementation is here.
See docs in GitHub Pages.
Documentation is generated by jazzy.
The type of the element that to take the differences must be conform to the Differentiable
protocol.
The differenceIdentifier
's type is determined generic by the associated type:
struct User: Differentiable {
let id: Int
let name: String
var differenceIdentifier: Int {
return id
}
func isContentEqual(to source: User) -> Bool {
return name == source.name
}
}
In the case of definition above, id
uniquely identifies the element and get to know the user updated by comparing equality of name
of the elements in source and target.
There are default implementations of Differentiable
for the types that conformed to Equatable
or Hashable
οΌ
public extension Differentiable where Self: Equatable {
func isContentEqual(to source: Self) -> Bool {
return self == source
}
}
public extension Differentiable where Self: Hashable {
var differenceIdentifier: Self {
return self
}
}
So, you can simply:
extension String: Differentiable {}
Calculates the differences by creating StagedChangeset
from two collections of elements conforming to Differentiable
:
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)
If you want to include multiple types conformed to Differentiable
in the collection, use AnyDifferentiable
:
let source = [
AnyDifferentiable("A"),
AnyDifferentiable(User(id: 0, name: "Vincent"))
]
In the case of sectioned collection, the section itself must have a unique identifier and be able to compare whether there is an update.
So each section must conform to DifferentiableSection
protocol, but in most cases you can use ArraySection
that general type conformed to it.
ArraySection
requires a model conforming to Differentiable
for differentiate from other sections:
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)
You can perform incremental updates on UITableView
and UICollectionView
using the created StagedChangeset
.
setData
closure. The differences are applied in stages, and failing to do so is bound to create a crash:
tableView.reload(using: changeset, with: .fade) { data in
dataSource.data = data
}
Batch-updates using too large amount of differences may adversely affect to performance.
Returning true
with interrupt
closure then falls back to reloadData
:
collectionView.reload(using: changeset, interrupt: { $0.changeCount > 100 }) { data in
dataSource.data = data
}
Made a fair comparison as much as possible in features and performance with other popular and awesome frameworks.
This does NOT determine superiority or inferiority of the frameworks. I know that each framework has different benefits.
The frameworks and its version that compared is below.
- DifferenceKit - master
- RxDataSources (Differentiator) - 3.0.2
- FlexibleDiff - 0.0.5
- IGListKit - 3.4.0
- ListDiff - 0.1.0
- DeepDiff - 1.2.0
- Differ (Diff.swift) - 1.2.3
- Dwifft - 0.8
Linear | Sectioned | Duplicate Element/Section | |
---|---|---|---|
DifferenceKit | β | β | β |
RxDataSources | β | β | β |
FlexibleDiff | β | β | β |
IGListKit | β | β | β |
ListDiff | β | β | β |
DeepDiff | β | β | β |
Differ | β | β | β |
Dwifft | β | β | β |
Linear
means 1-dimensional collection.
Sectioned
means 2-dimensional collection.
Delete | Insert | Move | Reload | Move across sections | |
---|---|---|---|---|---|
DifferenceKit | β | β | β | β | β |
RxDataSources | β | β | β | β | β |
FlexibleDiff | β | β | β | β | β |
IGListKit | β | β | β | β | β |
ListDiff | β | β | β | β | β |
DeepDiff | β | β | β | β / β | β |
Differ | β | β | β | β | β |
Dwifft | β | β | β | β | β |
Delete | Insert | Move | Reload | |
---|---|---|---|---|
DifferenceKit | β | β | β | β |
RxDataSources | β | β | β | β |
FlexibleDiff | β | β | β | β |
IGListKit | β | β | β | β |
ListDiff | β | β | β | β |
DeepDiff | β | β | β | β |
Differ | β | β | β | β |
Dwifft | β | β | β | β |
Performance was measured using XCTestCase.measure
on iPhoneX simulator with -O -whole-module-optimization
.
Use Foundation.UUID
as an element.
Time(second) | |
---|---|
DifferenceKit | 0.0032 |
RxDataSources | 0.0078 |
FlexibleDiff | 0.0168 |
IGListKit | 0.0412 |
ListDiff | 0.0388 |
DeepDiff | 0.0150 |
Differ | 0.3260 |
Dwifft | 33.600 |
Time(second) | |
---|---|
DifferenceKit | 0.0076 |
RxDataSources | 0.0143 |
FlexibleDiff | 0.0305 |
IGListKit | 0.0891 |
ListDiff | 0.0802 |
DeepDiff | 0.0300 |
Differ | 1.3450 |
Dwifft | β |
Time(second) | |
---|---|
DifferenceKit | 0.087 |
RxDataSources | 0.179 |
FlexibleDiff | 0.356 |
IGListKit | 1.329 |
ListDiff | 1.026 |
DeepDiff | 0.334 |
Differ | β |
Dwifft | β |
- Swift4.2+
- iOS 9.0+
- tvOS 9.0+
- OS X 10.9+
- watchOS 2.0+ (only algorithm)
To use only algorithm without extensions for UI, add the following:
use_frameworks!
target 'TargetName' do
pod 'DifferenceKit/Core'
end
To use DifferenceKit with UIKit extension, add the following to your Podfile
:
target 'TargetName' do
pod 'DifferenceKit'
end
or
target 'TargetName' do
pod 'DifferenceKit/UIKitExtension'
end
To use DifferenceKit with AppKit extension, add the following to your Podfile
:
target 'TargetName' do
pod 'DifferenceKit/AppKitExtension'
end
There is no UI extension for watchOS.
You can use only the algorithm.
Add the following to your Cartfile
:
github "ra1028/DifferenceKit"
And run
carthage update
Welcome to fork and submit pull requests.
Before submitting pull request, please ensure you have passed the included tests.
If your pull request including new function, please write test cases for it.
DifferenceKit is released under the MIT License.