Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/ra1028/DifferenceKit

💻 A fast and flexible O(n) difference algorithm framework for Swift collection.
https://github.com/ra1028/DifferenceKit

algorithm changeset collectionview diff difference diffing paul-heckel-algorithm tableview

Last synced: about 1 month ago
JSON representation

💻 A fast and flexible O(n) difference algorithm framework for Swift collection.

Awesome Lists containing this project

README

        




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


Swift5
Release
CocoaPods
Carthage
Swift Package Manager

CI Status
Platform
Lincense



Made with ❤️ by Ryo Aoyama and Contributors


---

## Features

💡 Fastest **O(n)** diffing algorithm optimized for Swift collection

💡 Calculate diffs for batch updates of list UI in `UIKit`, `AppKit` and [Texture](https://github.com/TextureGroup/Texture)

💡 Supports both linear and sectioned collection even if contains duplicates

💡 Supports **all kind of diffs** for animated UI batch updates

---

## Algorithm

This is a diffing algorithm developed for [Carbon](https://github.com/ra1028/Carbon), works stand alone.
The algorithm optimized based on the Paul Heckel's algorithm.
See also his paper ["A technique for isolating differences between files"](https://dl.acm.org/citation.cfm?id=359467) released in 1978.
It allows all kind of diffs to be calculated in linear time **O(n)**.
[RxDataSources](https://github.com/RxSwiftCommunity/RxDataSources) and [IGListKit](https://github.com/Instagram/IGListKit) are also implemented based on his algorithm.

However, in `performBatchUpdates` of `UITableView`, `UICollectionView`, etc, there are combinations of diffs that cause crash when applied simultaneously.
To solve this problem, `DifferenceKit` takes an approach of split the set of diffs at the minimal stages that can be perform batch updates with no crashes.

Implementation is [here](https://github.com/ra1028/DifferenceKit/blob/master/Sources/Algorithm.swift).

---

## Getting Started

- [API Documentation](https://ra1028.github.io/DifferenceKit)
- [Example Apps](https://github.com/ra1028/DifferenceKit/blob/master/Examples)
- [Benchmark](https://github.com/ra1028/DifferenceKit/blob/master/Benchmark)
- [Playground](https://github.com/ra1028/DifferenceKit/blob/master/DifferenceKit.playground/Contents.swift)

## Basic Usage

The type of the element that to take diffs must be conform to the `Differentiable` protocol.
The `differenceIdentifier`'s type is generic associated type:
```swift
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 conforming to `Equatable` or `Hashable`:
```swift
// If `Self` conforming to `Hashable`.
var differenceIdentifier: Self {
return self
}

// If `Self` conforming to `Equatable`.
func isContentEqual(to source: Self) -> Bool {
return self == source
}
```
Therefore, you can simply:
```swift
extension String: Differentiable {}
```

Calculate the diffs by creating `StagedChangeset` from two collections of elements conforming to `Differentiable`:
```swift
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 conforming to `Differentiable` in the collection, use `AnyDifferentiable`:
```swift
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 conforming to `DifferentiableSection` protocol, but in most cases you can use `ArraySection` that general type conforming to it.
`ArraySection` requires a model conforming to `Differentiable` for diffing from other sections:
```swift
enum Model: Differentiable {
case a, b, c
}

let source: [ArraySection] = [
ArraySection(model: .a, elements: ["A", "B"]),
ArraySection(model: .b, elements: ["C"])
]
let target: [ArraySection] = [
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 diffing batch updates of `UITableView` and `UICollectionView` using the created `StagedChangeset`.

⚠️ **Don't forget** to **synchronously** update the data referenced by the data-source, with the data passed in the `setData` closure. The diffs are applied in stages, and failing to do so is bound to create a crash:

```swift
tableView.reload(using: changeset, with: .fade) { data in
dataSource.data = data
}
```

Batch updates using too large amount of diffs may adversely affect to performance.
Returning `true` with `interrupt` closure then falls back to `reloadData`:
```swift
collectionView.reload(using: changeset, interrupt: { $0.changeCount > 100 }) { data in
dataSource.data = data
}
```


[See More Usage]

---

## Comparison with Other Frameworks

Made a fair comparison as much as possible in performance and features 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](https://github.com/ra1028/DifferenceKit) - master
- [RxDataSources](https://github.com/RxSwiftCommunity/RxDataSources) ([Differentiator](https://github.com/RxSwiftCommunity/RxDataSources/tree/master/Sources/Differentiator)) - 4.0.1
- [FlexibleDiff](https://github.com/RACCommunity/FlexibleDiff) - 0.0.8
- [IGListKit](https://github.com/Instagram/IGListKit) - 3.4.0
- [DeepDiff](https://github.com/onmyway133/DeepDiff) - 2.2.0
- [Differ](https://github.com/tonyarnold/Differ) ([Diff.swift](https://github.com/wokalski/Diff.swift)) - 1.4.3
- [Dwifft](https://github.com/jflinter/Dwifft) - 0.9
- [Swift.CollectionDifference](https://developer.apple.com/documentation/swift/collectiondifference) - Swift 5.1

### Performance Comparison

Benchmark project is [here](https://github.com/ra1028/DifferenceKit/blob/master/Benchmark).
Performance was mesured by code compiled using `Xcode11.1` and `Swift 5.1` with `-O` optimization and run on `iPhone11 Pro simulator`.
Use `Foundation.UUID` as an element of collections.

#### - From 5,000 elements to 1,000 deleted, 1,000 inserted and 200 shuffled

| |Time(sec) |
|:-------------------------|-------------------------:|
|DifferenceKit |`0.0019` |
|RxDataSources |`0.0074` |
|IGListKit |`0.0346` |
|FlexibleDiff |`0.0161` |
|DeepDiff |`0.0373` |
|Differ |`1.0581` |
|Dwifft |`0.4732` |
|Swift.CollectionDifference|`0.0620` |

#### - From 100,000 elements to 10,000 deleted, 10,000 inserted and 2,000 shuffled

| |Time(sec) |
|:-------------------------|-------------------------:|
|DifferenceKit |`0.0348` |
|RxDataSources |`0.1024` |
|IGListKit |`0.7002` |
|FlexibleDiff |`0.2189` |
|DeepDiff |`0.5537` |
|Differ |`153.8007` |
|Dwifft |`187.1341` |
|Swift.CollectionDifference|`5.0281` |

### Features Comparison

#### - Algorithm

| |Base algorithm|Order|
|:-------------------------|-------------:|----:|
|DifferenceKit |Heckel |O(N) |
|RxDataSources |Heckel |O(N) |
|FlexibleDiff |Heckel |O(N) |
|IGListKit |Heckel |O(N) |
|DeepDiff |Heckel |O(N) |
|Differ |Myers |O(ND)|
|Dwifft |Myers |O(ND)|
|Swift.CollectionDifference|Myers |O(ND)|

\* [**Heckel algorithm**](https://dl.acm.org/citation.cfm?id=359467)
\* [**Myers algorithm**](http://www.xmailserver.org/diff2.pdf)

#### - Supported Collection

| |Linear|Sectioned|Duplicate element/section|
|:-------------------------|:----:|:-------:|:-----------------------:|
|DifferenceKit |✅ |✅ |✅ |
|RxDataSources |❌ |✅ |❌ |
|FlexibleDiff |✅ |✅ |✅ |
|IGListKit |✅ |❌ |✅ |
|DeepDiff |✅ |❌ |✅ |
|Differ |✅ |✅ |✅ |
|Dwifft |✅ |✅ |✅ |
|Swift.CollectionDifference|✅ |❌ |✅ |

\* **Linear** means 1-dimensional collection
\* **Sectioned** means 2-dimensional collection

#### - Supported Element Diff

| |Delete|Insert|Move|Reload|Move across sections|
|:-------------------------|:----:|:----:|:--:|:----:|:------------------:|
|DifferenceKit |✅ |✅ |✅ |✅ |✅ |
|RxDataSources |✅ |✅ |✅ |✅ |✅ |
|FlexibleDiff |✅ |✅ |✅ |✅ |❌ |
|IGListKit |✅ |✅ |✅ |✅ |❌ |
|DeepDiff |✅ |✅ |✅ |✅ |❌ |
|Differ |✅ |✅ |✅ |❌ |❌ |
|Dwifft |✅ |✅ |❌ |❌ |❌ |
|Swift.CollectionDifference|✅ |✅ |✅ |❌ |❌ |

#### - Supported Section Diff

| |Delete|Insert|Move|Reload|
|:-------------------------|:----:|:----:|:--:|:----:|
|DifferenceKit |✅ |✅ |✅ |✅ |
|RxDataSources |✅ |✅ |✅ |❌ |
|FlexibleDiff |✅ |✅ |✅ |✅ |
|IGListKit |❌ |❌ |❌ |❌ |
|DeepDiff |❌ |❌ |❌ |❌ |
|Differ |✅ |✅ |✅ |❌ |
|Dwifft |✅ |✅ |❌ |❌ |
|Swift.CollectionDifference|❌ |❌ |❌ |❌ |

---

## Requirements

- Swift 4.2+
- iOS 9.0+
- tvOS 9.0+
- OS X 10.9+
- watchOS 2.0+ (only algorithm)

---

## Installation

### [CocoaPods](https://cocoapods.org/)

To use only algorithm without extensions for UI, add the following to your `Podfile`:
```ruby
pod 'DifferenceKit/Core'
```

#### iOS / tvOS

To use DifferenceKit with UIKit extension, add the following to your `Podfile`:
```ruby
pod 'DifferenceKit'
```
or
```ruby
pod 'DifferenceKit/UIKitExtension'
```

#### macOS

To use DifferenceKit with AppKit extension, add the following to your `Podfile`:
```ruby
pod 'DifferenceKit/AppKitExtension'
```

#### watchOS

There is no UI extension for watchOS.
To use only algorithm without extensions for UI, add the following to your `Podfile`:
```ruby
pod 'DifferenceKit/Core'
```

### [Carthage](https://github.com/Carthage/Carthage)

Add the following to your `Cartfile`:
```ruby
github "ra1028/DifferenceKit"
```

### [Swift Package Manager for Apple platforms](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app)

Select Xcode menu `File > Swift Packages > Add Package Dependency` and enter repository URL with GUI.
```
Repository: https://github.com/ra1028/DifferenceKit
```

### [Swift Package Manager](https://swift.org/package-manager/)

Add the following to the dependencies of your `Package.swift`:
```swift
.package(url: "https://github.com/ra1028/DifferenceKit.git", from: "version")
```

---

## Contribution

Pull requests, bug reports and feature requests are welcome 🚀
Please see the [CONTRIBUTING](https://github.com/ra1028/DifferenceKit/blob/master/CONTRIBUTING.md) file for learn how to contribute to DifferenceKit.

---

## Credit

#### Bibliography
DifferenceKit was developed with reference to the following excellent materials and framework.

- [A technique for isolating differences between files](https://dl.acm.org/citation.cfm?id=359467) (by [Paul Heckel](https://dl.acm.org/author_page.cfm?id=81100051772))
- [DifferenceAlgorithmComparison](https://github.com/horita-yuya/DifferenceAlgorithmComparison) (by [@horita-yuya](https://github.com/horita-yuya))

#### OSS using DifferenceKit
The list of the awesome OSS which uses this library. They also help to understanding how to use DifferenceKit.

- [Carbon](https://github.com/ra1028/Carbon) (by [@ra1028](https://github.com/ra1028))
- [DiffableDataSources](https://github.com/ra1028/DiffableDataSources) (by [@ra1028](https://github.com/ra1028))
- [Rocket.Chat.iOS](https://github.com/RocketChat/Rocket.Chat.iOS) (by [RocketChat](https://github.com/RocketChat))
- [wire-ios](https://github.com/wireapp/wire-ios) (by [Wire Swiss GmbH](https://github.com/wireapp))
- [ReactiveLists](https://github.com/plangrid/ReactiveLists) (by [PlanGrid](https://github.com/plangrid))
- [ReduxMovieDB](https://github.com/cardoso/ReduxMovieDB) (by [@cardoso](https://github.com/cardoso))
- [TetrisDiffingCompetition](https://github.com/skagedal/TetrisDiffingCompetition) (by [@skagedal](https://github.com/skagedal))

#### Other diffing libraries
I respect and ️❤️ all libraries involved in diffing.

- [RxDataSources](https://github.com/RxSwiftCommunity/RxDataSources) (by [@kzaher](https://github.com/kzaher), [RxSwift Community](https://github.com/RxSwiftCommunity))
- [IGListKit](https://github.com/Instagram/IGListKit) (by [Instagram](https://github.com/Instagram))
- [FlexibleDiff](https://github.com/RACCommunity/FlexibleDiff) (by [@andersio](https://github.com/andersio), [RACCommunity](https://github.com/RACCommunity))
- [DeepDiff](https://github.com/onmyway133/DeepDiff) (by [@onmyway133](https://github.com/onmyway133))
- [Differ](https://github.com/tonyarnold/Differ) (by [@tonyarnold](https://github.com/tonyarnold))
- [Dwifft](https://github.com/jflinter/Dwifft) (by [@jflinter](https://github.com/jflinter))
- [Changeset](https://github.com/osteslag/Changeset) (by [@osteslag](https://github.com/osteslag))

---

## License
DifferenceKit is released under the [Apache 2.0 License](https://github.com/ra1028/DifferenceKit/blob/master/LICENSE).