DifferenceKit
A fast and flexible O(n) difference algorithm framework for Swift collection.
The algorithm is optimized based on the Paul Heckel's algorithm.
Features
✅ Automate to calculate commands for batch-updates of UITableView and UICollectionView
✅ 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 commands for animating UI batch-updates including section reloads
Sample app is imitated from DataSources ?
Algorithm
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 commands 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.
Documentation
⚠️ Since DifferenceKit is currently released in alpha version, updates may contain breaking changes.
See docs in GitHub Pages.
Documentation is generated by jazzy.
Getting Started
Example codes
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:
extension String: Differentiable {}
You can calculate 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 Section that general type conformed to it.
Section requires a model conforming to Differentiable for differentiate from other sections:
enum Model: Differentiable {
case a, b, c
}
let source: [Section<Model, String>] = [
Section(model: .a, elements: ["A", "B"]),
Section(model: .b, elements: ["C"])
]
let target: [Section<Model, String>] = [
Section(model: .c, elements: ["D", "E"]),
Section(model: .a, elements: ["A"]),
Section(model: .b, elements: ["B", "C"])
]
let changeset = StagedChangeset(source: source, target: target)
You can incremental updates UITableView and UICollectionView using the created StagedChangeset.
Don't forget to update the data referenced by the dataSource in setData closure, as the differences is applied in stages:
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 reloadDate:
collectionView.reload(using: changeset, interrupt: { $0.changeCount > 100 }) { data in
dataSource.data = data
}
Comparison with Other Frameworks
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
Features comparison
- Supported collection
| Linear | Sectioned | Duplicate Element/Section | |
|---|---|---|---|
| DifferenceKit | ✅ | ✅ | ✅ |
| RxDataSources | ❌ | ✅ | ❌ |
| FlexibleDiff | ✅ | ✅ | ✅ |
| IGListKit | ✅ | ❌ | ✅ |
| ListDiff | ✅ | ❌ | ✅ |
| DeepDiff | ✅ | ❌ | ✅ |
| Differ | ✅ | ✅ | ✅ |
| Dwifft | ✅ | ✅ | ✅ |
Linear means 1-dimensional collection.
Sectioned means 2-dimensional collection.
- Supported element differences
| Delete | Insert | Move | Reload | Move across sections | |
|---|---|---|---|---|---|
| DifferenceKit | ✅ | ✅ | ✅ | ✅ | ✅ |
| RxDataSources | ✅ | ✅ | ✅ | ✅ | ✅ |
| FlexibleDiff | ✅ | ✅ | ✅ | ✅ | ❌ |
| IGListKit | ✅ | ✅ | ✅ | ✅ | ❌ |
| ListDiff | ✅ | ✅ | ✅ | ✅ | ❌ |
| DeepDiff | ✅ | ✅ | ✅ | ✅ / ❌ | ❌ |
| Differ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Dwifft | ✅ | ✅ | ❌ | ❌ | ❌ |
- Supported section differences
| Delete | Insert | Move | Reload | |
|---|---|---|---|---|
| DifferenceKit | ✅ | ✅ | ✅ | ✅ |
| RxDataSources | ✅ | ✅ | ✅ | ❌ |
| FlexibleDiff | ✅ | ✅ | ✅ | ✅ |
| IGListKit | ❌ | ❌ | ❌ | ❌ |
| ListDiff | ❌ | ❌ | ❌ | ❌ |
| DeepDiff | ❌ | ❌ | ❌ | ❌ |
| Differ | ✅ | ✅ | ✅ | ❌ |
| Dwifft | ✅ | ✅ | ❌ | ❌ |
Performance comparison
Performance was measured using XCTestCase.measure on iPhoneX simulator with -O -whole-module-optimization.
Use Foundation.UUID as an element.
⚠️ If Move is included in the difference, performance may obviously decrease in some frameworks.
⚠️ DeepDiff may had increased the processing speed by misuse of Hashable in algorithm.
- From 5,000 elements to 500 deleted and 500 inserted
| Time(second) | |
|---|---|
| DifferenceKit | 0.00425 |
| RxDataSources | 0.00784 |
| FlexibleDiff | 0.0168 |
| IGListKit | 0.0412 |
| ListDiff | 0.0388 |
| DeepDiff | 0.015 |
| Differ | 0.326 |
| Dwifft | 33.6 |
- From 10,000 elements to 1,000 deleted and 1,000 inserted
| Time(second) | |
|---|---|
| DifferenceKit | 0.0079 |
| RxDataSources | 0.0143 |
| FlexibleDiff | 0.0305 |
| IGListKit | 0.0891 |
| ListDiff | 0.0802 |
| DeepDiff | 0.030 |
| Differ | 1.345 |
| Dwifft | ❌ |
- From 100,000 elements to 10,000 deleted and 10,000 inserted
| Time(second) | |
|---|---|
| DifferenceKit | 0.098 |
| RxDataSources | 0.179 |
| FlexibleDiff | 0.356 |
| IGListKit | 1.329 |
| ListDiff | 1.026 |
| DeepDiff | 0.334 |
| Differ | ❌ |
| Dwifft | ❌ |
Requirements
- Swift4.1+
- iOS 9.0+
- tvOS 9.0+
- OS X 10.9+ (only algorithm)
- watchOS 2.0+ (only algorithm)
Installation
CocoaPods
Add the following to your Podfile:
use_frameworks!
target 'TargetName' do
pod 'DifferenceKit'
end
To use only algorithm without extensions for UI, add the following:
use_frameworks!
target 'TargetName' do
pod 'DifferenceKit/Core'
end
And run
pod install
Carthage
Add the following to your Cartfile:
github "ra1028/DifferenceKit"
And run
carthage update