Skip to content

Commit

Permalink
Fix observable binding and add array diff.
Browse files Browse the repository at this point in the history
  • Loading branch information
srdanrasic committed Nov 27, 2015
1 parent 599bdaa commit 9aeac15
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 12 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@ The resulting `sortedOptions` is of type `ObservableCollection<[(String, String)

> Same threading rules apply for observable collection bindings as for observable bindings. You can safely modify the collection from a background thread and be confident that the UI updates occur on the main thread.
### Array diff

When you need to replace an array with another array, but need an event to contains fine-grained changes (for example to update table/collection view with nice animations), you can use method `replace:performDiff:`. For example, if you have

```swift
let numbers: ObservableCollection([1, 2, 3])
```

and you do

```swift
numbers.replace([0, 1, 3, 4], performDiff: true)
```

then the observed event will contain:

```swift
Assert(event.collection == [0, 1, 3, 4])
Assert(event.inserts == [0, 3])
Assert(event.deletes == [1])
```

If that array was bound to a table or a collection view, the view would automatically animate only the changes from the *merge*. Helpful, isn't it.

## Operation

Expand Down Expand Up @@ -217,7 +240,7 @@ image.bindTo(imageView1)
image.bindTo(imageView2)
```

> Method `shareNext` buffers results of the operation using `ActiveStream` type. To learn more about that, continue reading.
> Method `shareNext` buffers results of the operation using `ObservableBuffer` type. To learn more about that, continue reading.
### Transformations

Expand Down
14 changes: 14 additions & 0 deletions ReactiveKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@
EC2C7A461C0314BC006BFEE1 /* StreamSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBCCDDF1BEB6B9B00723476 /* StreamSpec.swift */; };
EC2C7A471C0314BC006BFEE1 /* OperationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC835BE11BEC923400463098 /* OperationSpec.swift */; };
EC2C7A481C0314BC006BFEE1 /* ObservableCollectionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16EABACF1C01AD82008B20BD /* ObservableCollectionSpec.swift */; };
EC7591EC1C08710A001F31B3 /* ArrayDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7591EB1C08710A001F31B3 /* ArrayDiff.swift */; };
EC7591ED1C08710A001F31B3 /* ArrayDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7591EB1C08710A001F31B3 /* ArrayDiff.swift */; };
EC7591EE1C08710A001F31B3 /* ArrayDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7591EB1C08710A001F31B3 /* ArrayDiff.swift */; };
EC7591EF1C08710A001F31B3 /* ArrayDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7591EB1C08710A001F31B3 /* ArrayDiff.swift */; };
EC7591F11C0871EF001F31B3 /* ArrayDiffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7591F01C0871EF001F31B3 /* ArrayDiffTests.swift */; };
EC9549B31BF1EFA2000FC2BF /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1648A99E1BF12CE9007A185C /* Result.swift */; };
EC9549B41BF1EFA3000FC2BF /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1648A99E1BF12CE9007A185C /* Result.swift */; };
EC9549B51BF1EFA4000FC2BF /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1648A99E1BF12CE9007A185C /* Result.swift */; };
Expand Down Expand Up @@ -164,6 +169,8 @@
EC0DF29E1BF4909C00DFF3E6 /* MutableObservableCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableObservableCollection.swift; sourceTree = "<group>"; };
EC2C7A0C1C02ED71006BFEE1 /* PerformanceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceTests.swift; sourceTree = "<group>"; };
EC2C7A411C031030006BFEE1 /* ObservableBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ObservableBuffer.swift; path = ReactiveKit/ObservableBuffer/ObservableBuffer.swift; sourceTree = SOURCE_ROOT; };
EC7591EB1C08710A001F31B3 /* ArrayDiff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayDiff.swift; sourceTree = "<group>"; };
EC7591F01C0871EF001F31B3 /* ArrayDiffTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayDiffTests.swift; sourceTree = "<group>"; };
EC835BE11BEC923400463098 /* OperationSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationSpec.swift; sourceTree = "<group>"; };
EC835C001BECB0FB00463098 /* ReactiveKitPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = ReactiveKitPlayground.playground; sourceTree = SOURCE_ROOT; };
ECBCCDD01BEB6B9A00723476 /* ReactiveKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactiveKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -317,6 +324,7 @@
EC835BE11BEC923400463098 /* OperationSpec.swift */,
16EABACF1C01AD82008B20BD /* ObservableCollectionSpec.swift */,
EC2C7A0C1C02ED71006BFEE1 /* PerformanceTests.swift */,
EC7591F01C0871EF001F31B3 /* ArrayDiffTests.swift */,
16F0B8791BEFC3A10071847A /* Dependencies */,
ECBCCDE11BEB6B9B00723476 /* Info.plist */,
);
Expand All @@ -341,6 +349,7 @@
children = (
ECBCCDF21BEB6BBE00723476 /* Lock.swift */,
ECBCCDF31BEB6BBE00723476 /* Reference.swift */,
EC7591EB1C08710A001F31B3 /* ArrayDiff.swift */,
);
path = Internals;
sourceTree = "<group>";
Expand Down Expand Up @@ -627,6 +636,7 @@
16C33B341BEFBA2D00A0DBE0 /* Observable.swift in Sources */,
16C33B331BEFBA2D00A0DBE0 /* Stream+Operation.swift in Sources */,
EC0DF29B1BF48FF400DFF3E6 /* MutableObservable.swift in Sources */,
EC7591ED1C08710A001F31B3 /* ArrayDiff.swift in Sources */,
16C33B431BEFBA2D00A0DBE0 /* NoError.swift in Sources */,
16C33B321BEFBA2D00A0DBE0 /* OperationSink.swift in Sources */,
16C33B401BEFBA2D00A0DBE0 /* Lock.swift in Sources */,
Expand Down Expand Up @@ -664,6 +674,7 @@
16C33B501BEFBA2D00A0DBE0 /* Observable.swift in Sources */,
16C33B4F1BEFBA2D00A0DBE0 /* Stream+Operation.swift in Sources */,
EC0DF29C1BF48FF400DFF3E6 /* MutableObservable.swift in Sources */,
EC7591EE1C08710A001F31B3 /* ArrayDiff.swift in Sources */,
16C33B5F1BEFBA2D00A0DBE0 /* NoError.swift in Sources */,
16C33B4E1BEFBA2D00A0DBE0 /* OperationSink.swift in Sources */,
16C33B5C1BEFBA2D00A0DBE0 /* Lock.swift in Sources */,
Expand Down Expand Up @@ -701,6 +712,7 @@
16C33B6C1BEFBA2E00A0DBE0 /* Observable.swift in Sources */,
16C33B6B1BEFBA2E00A0DBE0 /* Stream+Operation.swift in Sources */,
EC0DF29D1BF48FF400DFF3E6 /* MutableObservable.swift in Sources */,
EC7591EF1C08710A001F31B3 /* ArrayDiff.swift in Sources */,
16C33B7B1BEFBA2E00A0DBE0 /* NoError.swift in Sources */,
16C33B6A1BEFBA2E00A0DBE0 /* OperationSink.swift in Sources */,
16C33B781BEFBA2E00A0DBE0 /* Lock.swift in Sources */,
Expand Down Expand Up @@ -738,6 +750,7 @@
ECBCCE1F1BEB6BBE00723476 /* Queue.swift in Sources */,
EC0DF29A1BF48FF400DFF3E6 /* MutableObservable.swift in Sources */,
ECBCCE181BEB6BBE00723476 /* ObservableCollection+Dictionary.swift in Sources */,
EC7591EC1C08710A001F31B3 /* ArrayDiff.swift in Sources */,
ECBCCE311BEB6BE100723476 /* Operation.swift in Sources */,
ECBCCE331BEB6BE100723476 /* OperationSink.swift in Sources */,
ECBCCE191BEB6BBE00723476 /* ObservableCollection+Set.swift in Sources */,
Expand Down Expand Up @@ -768,6 +781,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
EC7591F11C0871EF001F31B3 /* ArrayDiffTests.swift in Sources */,
EC2C7A461C0314BC006BFEE1 /* StreamSpec.swift in Sources */,
EC2C7A481C0314BC006BFEE1 /* ObservableCollectionSpec.swift in Sources */,
EC2C7A471C0314BC006BFEE1 /* OperationSpec.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion ReactiveKit/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.6</string>
<string>1.0.7</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
83 changes: 83 additions & 0 deletions ReactiveKit/Internals/ArrayDiff.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// The MIT License (MIT)
//
// Copyright (c) 2015 Srdan Rasic (@srdanrasic)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

// Created by Dapeng Gao on 20/10/15.
// The central idea of this algorithm is taken from https://github.com/jflinter/Dwifft

internal enum DiffStep<T> {
case Insert(element: T, index: Int)
case Delete(element: T, index: Int)
}

extension Array where Element: Equatable {

internal static func diff(x: [Element], _ y: [Element]) -> [DiffStep<Element>] {

if x.count == 0 {
return zip(y, y.indices).map(DiffStep<Element>.Insert)
}

if y.count == 0 {
return zip(x, x.indices).map(DiffStep<Element>.Delete)
}

// Use dynamic programming to generate a table such that `table[i][j]` represents
// the length of the longest common substring (LCS) between `x[0..<i]` and `y[0..<j]`
let xLen = x.count, yLen = y.count
var table = [[Int]](count: xLen + 1, repeatedValue: [Int](count: yLen + 1, repeatedValue: 0))
for i in 1...xLen {
for j in 1...yLen {
if x[i - 1] == y[j - 1] {
table[i][j] = table[i - 1][j - 1] + 1
} else {
table[i][j] = max(table[i - 1][j], table[i][j - 1])
}
}
}

// Backtrack to find out the diff
var backtrack: [DiffStep<Element>] = []
for var i = xLen, j = yLen; i > 0 || j > 0; {
if i == 0 {
j--
backtrack.append(.Insert(element: y[j], index: j))
} else if j == 0 {
i--
backtrack.append(.Delete(element: x[i], index: i))
} else if table[i][j] == table[i][j - 1] {
j--
backtrack.append(.Insert(element: y[j], index: j))
} else if table[i][j] == table[i - 1][j] {
i--
backtrack.append(.Delete(element: x[i], index: i))
} else {
i--
j--
}
}

// Reverse the result
return backtrack.reverse()
}
}
6 changes: 5 additions & 1 deletion ReactiveKit/Observable/Observable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public final class Observable<Value>: ActiveStream<Value>, ObservableType {

public var value: Value {
didSet {
next(value)
super.next(value)
}
}

Expand All @@ -40,6 +40,10 @@ public final class Observable<Value>: ActiveStream<Value>, ObservableType {
super.init()
}

public override func next(event: Value) {
self.value = event
}

public override func observe(on context: ExecutionContext? = ImmediateOnMainExecutionContext, observer: Observer) -> DisposableType {
let disposable = super.observe(on: context, observer: observer)
observer(value)
Expand Down
60 changes: 51 additions & 9 deletions ReactiveKit/ObservableCollection/ObservableCollection+Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,50 @@
//

extension ObservableCollectionType where Collection == Array<Element> {

public mutating func append(x: Collection.Generator.Element) {

/// Append `newElement` to the array.
public mutating func append(newElement: Collection.Generator.Element) {
var new = collection
new.append(x)
new.append(newElement)
next(ObservableCollectionEvent(collection: new, inserts: [collection.count], deletes: [], updates: []))
}


/// Insert `newElement` at index `i`.
public mutating func insert(newElement: Collection.Generator.Element, atIndex: Int) {
var new = collection
new.insert(newElement, atIndex: atIndex)
next(ObservableCollectionEvent(collection: new, inserts: [atIndex], deletes: [], updates: []))
}


/// Insert elements `newElements` at index `i`.
public mutating func insertContentsOf(newElements: [Collection.Generator.Element], at index: Collection.Index) {
var new = collection
new.insertContentsOf(newElements, at: index)
next(ObservableCollectionEvent(collection: new, inserts: Array(index..<index+newElements.count), deletes: [], updates: []))
}


/// Remove and return the element at index i.
public mutating func removeAtIndex(index: Int) -> Collection.Generator.Element {
var new = collection
let element = new.removeAtIndex(index)
next(ObservableCollectionEvent(collection: new, inserts: [], deletes: [index], updates: []))
return element
}


/// Remove an element from the end of the array in O(1).
public mutating func removeLast() -> Collection.Generator.Element {
var new = collection
let element = new.removeLast()
next(ObservableCollectionEvent(collection: new, inserts: [], deletes: [new.count], updates: []))
return element
}


/// Remove all elements from the array.
public mutating func removeAll() {
let deletes = Array(0..<collection.count)
next(ObservableCollectionEvent(collection: [], inserts: [], deletes: deletes, updates: []))
}

public subscript(index: Collection.Index) -> Collection.Generator.Element {
get {
return collection[index]
Expand All @@ -72,3 +78,39 @@ extension ObservableCollectionType where Collection == Array<Element> {
}
}
}

extension ObservableCollectionType where Collection == Array<Element>, Element: Equatable, Index == Int {

/// Replace current array with the new array and send change events.
///
/// - Parameters:
/// - newCollection: The array to replace current array with.
/// - performDiff: When `true`, difference between the current array and the new array will be calculated
/// and the sent event will contain exact description of which elements were inserted and which deleted.\n
/// When `false`, the sent event contains current array indices as `deletes` indices and new array indices as
/// `insertes` indices.
///
/// - Complexity: O(1) if `performDiff == false`. Otherwise O(`collection.count * newCollection.count`).
public mutating func replace(newCollection: Collection, performDiff: Bool) {
if performDiff {
var inserts: [Int] = []
var deletes: [Int] = []

inserts.reserveCapacity(collection.count)
deletes.reserveCapacity(collection.count)

let diff = Collection.diff(collection, newCollection)

for diffStep in diff {
switch diffStep {
case .Insert(_, let index): inserts.append(index)
case .Delete(_, let index): deletes.append(index)
}
}

next(ObservableCollectionEvent(collection: newCollection, inserts: inserts, deletes: deletes, updates: []))
} else {
replace(newCollection)
}
}
}
61 changes: 61 additions & 0 deletions ReactiveKitTests/ArrayDiffTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// DiffTests.swift
// Bond
//
// Created by Dapeng Gao on 20/10/15.
// Copyright © 2015 Bond. All rights reserved.
//

// This test case is taken from https://github.com/jflinter/Dwifft

import XCTest
@testable import ReactiveKit

class DiffTests: XCTestCase {

struct TestCase {
let array1: [Character]
let array2: [Character]

let expectedDiff: String

init(_ a: String, _ b: String, _ expectedDiff: String) {
self.array1 = Array(a.characters)
self.array2 = Array(b.characters)
self.expectedDiff = expectedDiff
}
}

private func encodeDiff<T>(x: [DiffStep<T>]) -> String {

var result = ""

for step in x {
switch step {
case let .Insert(e, i): result += "+\(e)@\(i)"
case let .Delete(e, i): result += "-\(e)@\(i)"
}
}
return result
}

func testDiff() {

let tests: [TestCase] = [
TestCase("1234", "23", "-1@0-4@3"),
TestCase("0125890", "4598310", "-0@0-1@1-2@2+4@0-8@4+8@3+3@4+1@5"),
TestCase("BANANA", "KATANA", "-B@0+K@0-N@2+T@2"),
TestCase("1234", "1224533324", "+2@2+4@3+5@4+3@6+3@7+2@8"),
TestCase("thisisatest", "testing123testing", "-h@1-i@2+e@1+t@3-s@5-a@6+n@5+g@6+1@7+2@8+3@9+i@14+n@15+g@16"),
TestCase("HUMAN", "CHIMPANZEE", "+C@0-U@1+I@2+P@4+Z@7+E@8+E@9"),
]

for test in tests {

let diff = Array.diff(test.array1, test.array2)
let stringRepresentation = encodeDiff(diff)

XCTAssertEqual(stringRepresentation, test.expectedDiff)
}
}
}

0 comments on commit 9aeac15

Please sign in to comment.