Skip to content

Instantly share code, notes, and snippets.

@neilpa
Last active October 8, 2015 15:17
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save neilpa/cc64d42d64fc502d1457 to your computer and use it in GitHub Desktop.
Save neilpa/cc64d42d64fc502d1457 to your computer and use it in GitHub Desktop.
Building collections from signals[-of-signals] of collection mutations
//
// A half-baked approach (not thread-safe, poor disposable management) for creating
// aggregate collections from multiple signals of collection mutations. In this
// case we are concating multiple mutation streams into a single container. So for
// each inner signal we need to offset subsequent splices by the count of preceding
// items (which can be recovered by scanning previous splices).
//
// Example:
//
// let (first, sink1) = SignalProducer<Splice<Int>, NoError>.buffer()
// let (second, sink2) = SignalProducer<Splice<Int>, NoError>.buffer()
// sendNext(sink1, Splice(index: 0, remove: [], insert: [1, 2, 3]))
// sendNext(sink2, Splice(index: 0, remove: [], insert: [4, 5, 6]))
// sendNext(sink2, Splice(index: 2, remove: [6], insert: []))
//
// let combined = SignalProducer<SignalProducer<Splice<Int>, NoError>, NoError>(values: [first, second])
// combined |> concatSplices |> start(next: println)
//
// Output:
//
// @0 -[] +[1, 2, 3]
// @3 -[] +[4, 5, 6]
// @5 -[6] +[]
//
// Inspiration
// https://github.com/ReactiveCocoa/ReactiveCocoa/pull/1032
// https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1903
// https://github.com/jonsterling/Taliban-MVVM/issues/1
// https://github.com/ReactiveCocoa/ReactiveCocoa/pull/1228#discussion_r11776533
// Define collection mutations in terms of splices
public struct Splice<T>: Printable {
public let index: Int
// In most cases this could be simplified to just a count of removed
// items. However, tracking the actual items allows us to freely invert
// without additional context. That in turn can enable things like
// deferral of updates on "live" collections in cases where our UI isn't
// ready to accept the update yet (e.g. in the middle of an animation).
public let remove: [T]
public let insert: [T]
public func translate(amount: Int) -> Splice {
return Splice(index: index + amount, remove: remove, insert: insert)
}
public var delta: Int {
return insert.count - remove.count
}
public var description: String {
return "@\(index) -\(remove) +\(insert)"
}
}
// For lack of a better name at the moment
// TODO Could just use a Range
public struct Jump {
public let offset: Int
public let count: Int
public func successor() -> Jump {
return Jump(offset: offset + count, count: 0)
}
public func resize(delta: Int) -> Jump {
return Jump(offset: offset, count: count + delta)
}
public func translate(amount: Int) -> Jump {
return Jump(offset: offset + amount, count: count)
}
}
public func concatSplices<T>(producer: SignalProducer<SignalProducer<Splice<T>, NoError>, NoError>) -> SignalProducer<Splice<T>, NoError> {
return SignalProducer { observer, disposable in
var tail = Node(value: Jump(offset: 0, count: 0), previous: nil)
let tryComplete: () -> () = { _ in
if tail.previous == nil {
sendCompleted(observer)
}
}
producer
|> on(terminated: tryComplete)
|> start(next: { innerProducer in
tail = Node(value: tail.value.successor(), previous: tail)
let node = tail
innerProducer
|> on(terminated: { _ in
node.drop()
tryComplete()
})
|> start(next: { splice in
sendNext(observer, splice.translate(node.value.offset))
// Propagate index offset updates
if splice.delta != 0 {
node.value = node.value.resize(splice.delta)
var previous = node
while let next = previous.next {
next.value = next.value.translate(splice.delta)
previous = next
}
}
})
}, completed: {
tryComplete()
})
}
}
// Dead-simple linked list
public final class Node<T> {
var value: T
var previous: Node?
var next: Node?
public init(value: T, previous: Node?) {
self.value = value
self.previous = previous
next = previous?.next
previous?.next = self
}
public func drop() {
next?.previous = previous
previous?.next = next
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment