Skip to content

Commit

Permalink
Update README
Browse files Browse the repository at this point in the history
  • Loading branch information
ra1028 committed Aug 2, 2018
1 parent c87ee21 commit 34e3b43
Show file tree
Hide file tree
Showing 20 changed files with 612 additions and 72 deletions.
6 changes: 0 additions & 6 deletions DifferenceKit.playground/timeline.xctimeline

This file was deleted.

198 changes: 185 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,42 +23,214 @@ The algorithm is optimized based on the Paul Heckel's algorithm.
---

## Features
Coming soon.
✅ 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

---

## Introduction
Coming soon.
## 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](https://github.com/RxSwiftCommunity/RxDataSources) and [IGListKit](https://github.com/Instagram/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.

## Algorithm
Coming soon.
Implementation is [here]().

---

## Documentation
Coming soon.
See docs in [GitHub Pages]().
Documentation is generated by [jazzy](https://github.com/realm/jazzy).

---

## Getting Started
- [Example projects](./Examples)
- Example codes:
Coming soon.
- [Example app](./Examples)
- [Playground](./DifferenceKit.playground/Contents.swift)

### Example codes:
The type of the element that to take the differences must be conform to the `Differentiable` protocol:
```swift
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:
```swift
extension String: Differentiable {}
```

You can calculate the differences 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 conformed 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 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:
```swift
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:
```swift
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`:
```swift
collectionView.reload(using: changeset, interrupt: { $0.changeCount > 100 }) { data in
dataSource.data = data
}
```

---

## Comparison with Other Frameworks
Coming soon.
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.

- [DifferenceKit](https://github.com/ra1028/DifferenceKit) - 0.1.0
- [RxDataSources](https://github.com/RxSwiftCommunity/RxDataSources) ([Differentiator](https://github.com/RxSwiftCommunity/RxDataSources/tree/master/Sources/Differentiator)) - 3.0.2
- [IGListKit](https://github.com/Instagram/IGListKit) - 3.4.0
- [ListDiff](https://github.com/lxcid/ListDiff) - 0.1.0
- [Differ](https://github.com/tonyarnold/Differ) ([Diff.swift](https://github.com/wokalski/Diff.swift)) - 1.2.3
- [Dwifft](https://github.com/jflinter/Dwifft) - 0.8

### 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+
- OS X 10.9+
- tvOS 9.0+
- iOS 10.0+
- tvOS 10.0+

---

Expand Down
4 changes: 2 additions & 2 deletions Sources/AnyDifferentiable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
/// extension String: Differentiable {}
/// extension Int: Differentiable {}
///
/// let source: [AnyDifferentiable] = [
/// let source = [
/// AnyDifferentiable("ABC"),
/// AnyDifferentiable(100)
/// ]
/// let target: [AnyDifferentiable] = [
/// let target = [
/// AnyDifferentiable("ABC"),
/// AnyDifferentiable(100),
/// AnyDifferentiable(200)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Changeset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public struct Changeset<Collection: Swift.Collection> {
public var elementMoved: [(source: ElementPath, target: ElementPath)]

/// The number of all changes.
public var changesCount: Int {
public var changeCount: Int {
return sectionDeleted.count
+ sectionInserted.count
+ sectionUpdated.count
Expand All @@ -40,7 +40,7 @@ public struct Changeset<Collection: Swift.Collection> {

/// A Boolean value indicating whether has changes.
public var hasChanges: Bool {
return changesCount > 0
return changeCount > 0
}

/// Creates a new `Changeset`.
Expand Down
8 changes: 4 additions & 4 deletions Sources/UIExtensions/UIKitExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ public extension UITableView {
}

for changeset in stagedChangeset {
if let interrupt = interrupt, interrupt(changeset) {
setData(stagedChangeset.last!.data)
if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data {
setData(data)
return reloadData()
}

Expand Down Expand Up @@ -147,8 +147,8 @@ public extension UICollectionView {
}

for changeset in stagedChangeset {
if let interrupt = interrupt, interrupt(changeset) {
setData(stagedChangeset.last!.data)
if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data {
setData(data)
return reloadData()
}

Expand Down
20 changes: 10 additions & 10 deletions Tests/ChangesetTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,42 @@ import XCTest
import DifferenceKit

final class ChangesetTestCase: XCTestCase {
func testChangesCount() {
func testchangeCount() {
let c1 = Changeset(data: [()], sectionDeleted: [0, 1])
XCTAssertEqual(c1.changesCount, 2)
XCTAssertEqual(c1.changeCount, 2)

let c2 = Changeset(data: [()], sectionInserted: [0, 1, 2])
XCTAssertEqual(c2.changesCount, 3)
XCTAssertEqual(c2.changeCount, 3)

let c3 = Changeset(data: [()], sectionUpdated: [0, 1, 2, 3])
XCTAssertEqual(c3.changesCount, 4)
XCTAssertEqual(c3.changeCount, 4)

let c4 = Changeset(data: [()], sectionMoved: [(source: 0, target: 1)])
XCTAssertEqual(c4.changesCount, 1)
XCTAssertEqual(c4.changeCount, 1)

let c5 = Changeset(
data: [()],
elementDeleted: [ElementPath(element: 0, section: 0), ElementPath(element: 1, section: 1)]
)
XCTAssertEqual(c5.changesCount, 2)
XCTAssertEqual(c5.changeCount, 2)

let c6 = Changeset(
data: [()],
elementInserted: [ElementPath(element: 0, section: 0), ElementPath(element: 1, section: 1)]
)
XCTAssertEqual(c6.changesCount, 2)
XCTAssertEqual(c6.changeCount, 2)

let c7 = Changeset(
data: [()],
elementUpdated: [ElementPath(element: 0, section: 0), ElementPath(element: 1, section: 1)]
)
XCTAssertEqual(c7.changesCount, 2)
XCTAssertEqual(c7.changeCount, 2)

let c8 = Changeset(
data: [()],
elementMoved: [(source: ElementPath(element: 0, section: 0), target: ElementPath(element: 1, section: 1))]
)
XCTAssertEqual(c8.changesCount, 1)
XCTAssertEqual(c8.changeCount, 1)

let c9 = Changeset(
data: [()],
Expand All @@ -50,7 +50,7 @@ final class ChangesetTestCase: XCTestCase {
elementUpdated: [ElementPath(element: 9, section: 10)],
elementMoved: [(source: ElementPath(element: 11, section: 12), target: ElementPath(element: 13, section: 14))]
)
XCTAssertEqual(c9.changesCount, 8)
XCTAssertEqual(c9.changeCount, 8)
}

func testHasChanges() {
Expand Down
2 changes: 1 addition & 1 deletion docs/Extensions.html
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ <h4>Declaration</h4>
</section>
</section>
<section id="footer">
<p>&copy; 2018 <a class="link" href="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/ra1028" target="_blank" rel="external">Ryo Aoyama</a>. All rights reserved. (Last updated: 2018-08-01)</p>
<p>&copy; 2018 <a class="link" href="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/ra1028" target="_blank" rel="external">Ryo Aoyama</a>. All rights reserved. (Last updated: 2018-08-03)</p>
<p>Generated by <a class="link" href="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/realm/jazzy" target="_blank" rel="external">jazzy ♪♫ v0.9.3</a>, a <a class="link" href="https://realm.io" target="_blank" rel="external">Realm</a> project.</p>
</section>
</article>
Expand Down
2 changes: 1 addition & 1 deletion docs/Extensions/UICollectionView.html
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ <h4>Parameters</h4>
</section>
</section>
<section id="footer">
<p>&copy; 2018 <a class="link" href="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/ra1028" target="_blank" rel="external">Ryo Aoyama</a>. All rights reserved. (Last updated: 2018-08-01)</p>
<p>&copy; 2018 <a class="link" href="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/ra1028" target="_blank" rel="external">Ryo Aoyama</a>. All rights reserved. (Last updated: 2018-08-03)</p>
<p>Generated by <a class="link" href="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/realm/jazzy" target="_blank" rel="external">jazzy ♪♫ v0.9.3</a>, a <a class="link" href="https://realm.io" target="_blank" rel="external">Realm</a> project.</p>
</section>
</article>
Expand Down
2 changes: 1 addition & 1 deletion docs/Extensions/UITableView.html
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ <h4>Parameters</h4>
</section>
</section>
<section id="footer">
<p>&copy; 2018 <a class="link" href="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/ra1028" target="_blank" rel="external">Ryo Aoyama</a>. All rights reserved. (Last updated: 2018-08-01)</p>
<p>&copy; 2018 <a class="link" href="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/ra1028" target="_blank" rel="external">Ryo Aoyama</a>. All rights reserved. (Last updated: 2018-08-03)</p>
<p>Generated by <a class="link" href="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/realm/jazzy" target="_blank" rel="external">jazzy ♪♫ v0.9.3</a>, a <a class="link" href="https://realm.io" target="_blank" rel="external">Realm</a> project.</p>
</section>
</article>
Expand Down
2 changes: 1 addition & 1 deletion docs/Protocols.html
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ <h4>Declaration</h4>
</section>
</section>
<section id="footer">
<p>&copy; 2018 <a class="link" href="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/ra1028" target="_blank" rel="external">Ryo Aoyama</a>. All rights reserved. (Last updated: 2018-08-01)</p>
<p>&copy; 2018 <a class="link" href="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/ra1028" target="_blank" rel="external">Ryo Aoyama</a>. All rights reserved. (Last updated: 2018-08-03)</p>
<p>Generated by <a class="link" href="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/realm/jazzy" target="_blank" rel="external">jazzy ♪♫ v0.9.3</a>, a <a class="link" href="https://realm.io" target="_blank" rel="external">Realm</a> project.</p>
</section>
</article>
Expand Down
2 changes: 1 addition & 1 deletion docs/Protocols/Differentiable.html
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ <h4>Return Value</h4>
</section>
</section>
<section id="footer">
<p>&copy; 2018 <a class="link" href="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/ra1028" target="_blank" rel="external">Ryo Aoyama</a>. All rights reserved. (Last updated: 2018-08-01)</p>
<p>&copy; 2018 <a class="link" href="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/ra1028" target="_blank" rel="external">Ryo Aoyama</a>. All rights reserved. (Last updated: 2018-08-03)</p>
<p>Generated by <a class="link" href="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/realm/jazzy" target="_blank" rel="external">jazzy ♪♫ v0.9.3</a>, a <a class="link" href="https://realm.io" target="_blank" rel="external">Realm</a> project.</p>
</section>
</article>
Expand Down
Loading

0 comments on commit 34e3b43

Please sign in to comment.