Skip to content

πŸ’» A fast and flexible O(n) difference algorithm framework for Swift collection.

License

Notifications You must be signed in to change notification settings

rcholic/DifferenceKit

Repository files navigation

DifferenceKit

A fast and flexible O(n) difference algorithm framework for Swift collection.
The algorithm is optimized based on the Paul Heckel's algorithm.

Swift4 Platform Lincense
Build Status CocoaPods Carthage



Features

βœ… Calculate commands for batch-updates of UITableView and UICollectionView automatically
βœ… 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


Algorithm

The algorithm 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

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:

struct User: Differentiable {
    let id: Int
    let name: String

    var differenceIdentifier: Int {
        return id
    }

    func isUpdated(from 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 name of the elements in source and target.

There are default implementations of Differentiable for the types that conformed to Equatable or Hashable.
However, isUpdated(from:) is always returns false on the algorithm in default, so you can't know the update:

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.
The frameworks and its version that compared is below.

Features comparison

  • Supported collection
    Linear means 1-dimensional collection.
    Sectioned means 1-dimensional collection.
Linear Sectioned Duplicate Element/Section
DifferenceKit βœ… βœ… βœ…
RxDataSources ❌ βœ… ❌
IGListKit βœ… ❌ βœ…
ListDiff βœ… ❌ βœ…
Differ βœ… βœ… βœ…
Dwifft βœ… βœ… βœ…
  • Supported element differences
Delete Insert Move Reload
DifferenceKitοΏ½ βœ… βœ… βœ… βœ…
RxDataSources βœ… βœ… βœ… βœ…
IGListKit βœ… βœ… βœ… βœ…
ListDiff βœ… βœ… βœ… βœ…
Differ βœ… βœ… βœ… ❌
Dwifft βœ… βœ… ❌ ❌
  • Supported section differences
Delete Insert Move Reload
DifferenceKitοΏ½ βœ… βœ… βœ… βœ…
RxDataSources βœ… βœ… βœ… ❌
IGListKit ❌ ❌ ❌ ❌
ListDiff ❌ ❌ ❌ ❌
Differ βœ… βœ… βœ… ❌
Dwifft βœ… βœ… ❌ ❌

Performance comparison

Performance was measured using XCTestCase.measure with -O -whole-module-optimization.
Each framework uses a function that can compute as much differences as possible.
Use Foundation.UUID as an element.

  • From 5,000 elements to 500 deleted and 500 inserted
Time(second)
DifferenceKitοΏ½ 0.00425
RxDataSources 0.00784
IGListKit 0.0412
ListDiff 0.0388
Differ 0.449
Dwifft 33.6
  • From 10,000 elements to 1,000 deleted and 1,000 inserted
Time(second)
DifferenceKitοΏ½ 0.0079
RxDataSources 0.0143
IGListKit 0.0891
ListDiff 0.0802
Differ 1.788
Dwifft ❌
  • From 100,000 elements to 10,000 deleted and 10,000 inserted
Time(second)
DifferenceKitοΏ½ 0.098
RxDataSources 0.179
IGListKit 1.329
ListDiff 1.026
Differ ❌
Dwifft ❌

Requirements

  • Swift4.1+
  • iOS 10.0+
  • tvOS 10.0+

Installation

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

Add the following to your Cartfile:

github "ra1028/DifferenceKit"

And run

carthage update

Contribution

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.


License

DifferenceKit is released under the MIT License.

About

πŸ’» A fast and flexible O(n) difference algorithm framework for Swift collection.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Languages

  • Swift 98.6%
  • Ruby 1.4%