Skip to content
This repository has been archived by the owner on Oct 14, 2021. It is now read-only.

Commit

Permalink
Make IndefiniteObservable observer-agnostic.
Browse files Browse the repository at this point in the history
Summary:
IndefiniteObservable now makes no assumptions about the type of observer it can hold. Instead, a client is expected to subclass IndefiniteObservable and scope the Observer type accordingly:

For example, this is a single-channel ValueObserver:

```
public final class ValueObserver<T> {
  public typealias Value = T

  public init(_ next: @escaping (T) -> Void) {
    self.next = next
  }

  public let next: (T) -> Void
}

public class ValueObservable<T>: IndefiniteObservable<ValueObserver<T>> {
  public final func subscribe(_ next: @escaping (T) -> Void) -> Subscription {
    return super.subscribe(observer: ValueObserver(next))
  }
}
```

Reviewers: O4 Material Motion Apple platform reviewers, O2 Material Motion, markwei

Reviewed By: O4 Material Motion Apple platform reviewers, O2 Material Motion, markwei

Subscribers: markwei

Tags: #material_motion

Differential Revision: http://codereview.cc/D2116
  • Loading branch information
Jeff Verkoeyen committed Dec 6, 2016
1 parent 0a63084 commit 8c30e9a
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 64 deletions.
10 changes: 5 additions & 5 deletions examples/DelegateObservableExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,20 @@ public class DelegateObservableExampleViewController: UIViewController {
let pan = UIPanGestureRecognizer()
view.addGestureRecognizer(pan)

let dragStream = IndefiniteObservable<DragProducer.Value> { observer in
let dragStream = IndefiniteObservable { observer in
return DragProducer(subscribedTo: pan, observer: observer).unsubscribe
}

// Must hold a reference to the subscription, otherwise the stream will be deallocated when the
// subscription goes out of scope.
subscriptions.append(dragStream.subscribe {
subscriptions.append(dragStream.subscribe(observer: ValueObserver {
if $0.state == .began || $0.state == .changed {
targetView.layer.position = $0.location
}
})
}))

subscriptions.append(dragStream.subscribe {
subscriptions.append(dragStream.subscribe(observer: ValueObserver {
print($0.state.rawValue)
})
}))
}
}
81 changes: 70 additions & 11 deletions examples/OperatorExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,39 @@
import UIKit
import IndefiniteObservable

// This example demonstrates how to create custom operators that can be chained to an
// IndefiniteObservable.
// This example demonstrates how to create a custom observable/observer type and to add operators to
// it.

extension IndefiniteObservable {
public final class ValueObserver<T> {
public typealias Value = T

public init(_ next: @escaping (T) -> Void) {
self.next = next
}

public let next: (T) -> Void
}

public class ValueObservable<T>: IndefiniteObservable<ValueObserver<T>> {
public final func subscribe(_ next: @escaping (T) -> Void) -> Subscription {
return super.subscribe(observer: ValueObserver(next))
}
}

extension ValueObservable {

// Map from one value type to another.
public func map<U>(_ transform: @escaping (T) -> U) -> IndefiniteObservable<U> {
return IndefiniteObservable<U> { observer in
public func map<U>(_ transform: @escaping (T) -> U) -> ValueObservable<U> {
return ValueObservable<U> { observer in
return self.subscribe {
observer.next(transform($0))
}.unsubscribe
}
}

// Only emit values downstream for which passesTest returns true
public func filter(_ passesTest: @escaping (T) -> Bool) -> IndefiniteObservable<T> {
return IndefiniteObservable<T> { observer in
public func filter(_ passesTest: @escaping (T) -> Bool) -> ValueObservable<T> {
return ValueObservable<T> { observer in
return self.subscribe {
if passesTest($0) {
observer.next($0)
Expand All @@ -43,6 +59,43 @@ extension IndefiniteObservable {
}
}

public enum MotionState {
case atRest
case active
}

public final class MotionObserver<T> {
public typealias Value = T

public init(next: @escaping (T) -> Void, state: @escaping (MotionState) -> Void) {
self.next = next
self.state = state
}

public let next: (T) -> Void
public let state: (MotionState) -> Void
}

public class MotionObservable<T>: IndefiniteObservable<MotionObserver<T>> {
public final func subscribe(next: @escaping (T) -> Void, state: @escaping (MotionState) -> Void) -> Subscription {
return super.subscribe(observer: MotionObserver(next: next, state: state))
}
}

extension MotionObservable {

// Map from one value type to another.
public func map<U>(_ transform: @escaping (T) -> U) -> MotionObservable<U> {
return MotionObservable<U> { observer in
return self.subscribe(next: {
observer.next(transform($0))
}, state: { state in
observer.state(state)
}).unsubscribe
}
}
}

public class OperatorExampleViewController: UIViewController {

var initialPosition: CGPoint = .zero
Expand All @@ -59,11 +112,17 @@ public class OperatorExampleViewController: UIViewController {
let pan = UIPanGestureRecognizer()
view.addGestureRecognizer(pan)

let dragStream = IndefiniteObservable<DragProducer.Value> { observer in
let dragStream = ValueObservable<DragProducer.Value> { observer in
return DragProducer(subscribedTo: pan, observer: observer).unsubscribe
}

// Note that we avoid keep a strong reference to self in the stream's operators.
let motionStream = MotionObservable<Int> { observer in
observer.next(5)
observer.state(.atRest)
return noopUnsubscription
}

// Note that we avoid keeping a strong reference to self in the stream's operators.
// A strong reference would create a retain cycle:
//
// subscription -> stream -> operator -> self -> subscriptions
Expand All @@ -75,9 +134,9 @@ public class OperatorExampleViewController: UIViewController {
.filter { $0.state == .began || $0.state == .changed }
.map { $0.location }
.map { .init(x: midX, y: $0.y) }
.subscribe {
.subscribe(observer: ValueObserver {
targetView.layer.position = $0
}
})
)
}
}
24 changes: 5 additions & 19 deletions src/IndefiniteObservable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@
}
}
*/
public final class IndefiniteObservable<T> {
public typealias Subscriber<T> = (ValueObserver<T>) -> (() -> Void)?
open class IndefiniteObservable<O> {
public typealias Subscriber<O> = (O) -> (() -> Void)?

/** A subscriber is only invoked when subscribe is invoked. */
public init(_ subscriber: @escaping Subscriber<T>) {
public init(_ subscriber: @escaping Subscriber<O>) {
self.subscriber = subscriber
}

Expand All @@ -65,16 +65,15 @@ public final class IndefiniteObservable<T> {
- Parameter next: A block that will be executed when new values are sent from upstream.
- Returns: A subscription.
*/
public func subscribe(next: @escaping (T) -> Void) -> Subscription {
let observer = ValueObserver<T>(next)
public final func subscribe(observer: O) -> Subscription {
if let subscription = subscriber(observer) {
return SimpleSubscription(subscription)
} else {
return SimpleSubscription()
}
}

private let subscriber: Subscriber<T>
private let subscriber: Subscriber<O>
}

/** A Subscription is returned by IndefiniteObservable.subscribe. */
Expand All @@ -97,19 +96,6 @@ public protocol Subscription {
*/
public let noopUnsubscription: (() -> Void)? = nil

// MARK: Type erasing

/** An ValueObserver receives data from an IndefiniteObservable. */
public final class ValueObserver<T> {
public typealias Value = T

public init(_ next: @escaping (T) -> Void) {
self.next = next
}

public let next: (T) -> Void
}

// MARK: Private

// Internal class for ensuring that an active subscription keeps its stream alive.
Expand Down
16 changes: 8 additions & 8 deletions tests/unit/MemoryLeakTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import IndefiniteObservable

class MemoryLeakTests: XCTestCase {
func testObservableIsDeallocated() {
var observable: IndefiniteObservable<CGFloat>? = IndefiniteObservable<CGFloat> { observer in
var observable: ValueObservable<CGFloat>? = ValueObservable<CGFloat> { observer in
observer.next(5)
return noopUnsubscription
}
Expand All @@ -37,7 +37,7 @@ class MemoryLeakTests: XCTestCase {
}

func testDownstreamObservableKeepsUpstreamAlive() {
var observable: IndefiniteObservable<CGFloat>? = IndefiniteObservable<CGFloat> { observer in
var observable: ValueObservable<CGFloat>? = ValueObservable<CGFloat> { observer in
observer.next(5)
return noopUnsubscription
}
Expand All @@ -56,7 +56,7 @@ class MemoryLeakTests: XCTestCase {
}

func testSubscribedObservableIsDeallocated() {
var observable: IndefiniteObservable<CGFloat>? = IndefiniteObservable<CGFloat> { observer in
var observable: ValueObservable<CGFloat>? = ValueObservable<CGFloat> { observer in
observer.next(5)
return noopUnsubscription
}
Expand All @@ -76,7 +76,7 @@ class MemoryLeakTests: XCTestCase {
}

func testSubscribedObservableWithOperatorIsDeallocated() {
var observable: IndefiniteObservable<CGFloat>? = IndefiniteObservable<CGFloat> { observer in
var observable: ValueObservable<CGFloat>? = ValueObservable<CGFloat> { observer in
observer.next(5)
return noopUnsubscription
}
Expand All @@ -98,9 +98,9 @@ class MemoryLeakTests: XCTestCase {
}

func testUnsubscribedObservableWithOperatorIsDeallocated() {
weak var weakObservable: IndefiniteObservable<CGFloat>?
weak var weakObservable: ValueObservable<CGFloat>?
autoreleasepool {
let observable: IndefiniteObservable<CGFloat>? = IndefiniteObservable<CGFloat> { observer in
let observable: ValueObservable<CGFloat>? = ValueObservable<CGFloat> { observer in
observer.next(5)
return noopUnsubscription
}
Expand All @@ -121,12 +121,12 @@ class MemoryLeakTests: XCTestCase {
}

func testSubscriptionDoesNotKeepObservableInMemory() {
weak var weakObservable: IndefiniteObservable<Int>?
weak var weakObservable: ValueObservable<Int>?
var subscription: Subscription?

autoreleasepool {
let value = 10
let observable = IndefiniteObservable<Int> { observer in
let observable = ValueObservable<Int> { observer in
observer.next(value)
return noopUnsubscription
}
Expand Down
24 changes: 12 additions & 12 deletions tests/unit/ObservableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class ObservableTests: XCTestCase {
func testSubscription() {
let value = 10

let observable = IndefiniteObservable<Int> { observer in
let observable = ValueObservable<Int> { observer in
observer.next(value)
return noopUnsubscription
}
Expand All @@ -42,7 +42,7 @@ class ObservableTests: XCTestCase {
var didUnsubscribe = false

autoreleasepool {
let observable = IndefiniteObservable<CGFloat> { observer in
let observable = ValueObservable<CGFloat> { observer in
return {
didUnsubscribe = true
}
Expand All @@ -57,7 +57,7 @@ class ObservableTests: XCTestCase {
func testUnsubscribesOnUnsubscribe() {
var didUnsubscribe = false

let observable = IndefiniteObservable<CGFloat> { observer in
let observable = ValueObservable<CGFloat> { observer in
return {
didUnsubscribe = true
}
Expand All @@ -72,7 +72,7 @@ class ObservableTests: XCTestCase {
func testTwoSubsequentSubscriptions() {
let value = 10

let observable = IndefiniteObservable<Int> { observer in
let observable = ValueObservable<Int> { observer in
observer.next(value)
return noopUnsubscription
}
Expand All @@ -97,7 +97,7 @@ class ObservableTests: XCTestCase {
func testTwoParalellSubscriptions() {
let value = 10

let observable = IndefiniteObservable<Int> { observer in
let observable = ValueObservable<Int> { observer in
observer.next(value)
return noopUnsubscription
}
Expand All @@ -124,7 +124,7 @@ class ObservableTests: XCTestCase {

func testMappingValues() {
let value = 10
let observable = IndefiniteObservable<Int> { observer in
let observable = ValueObservable<Int> { observer in
observer.next(value)
return noopUnsubscription
}
Expand All @@ -141,7 +141,7 @@ class ObservableTests: XCTestCase {

func testMappingTypes() {
let value = CGPoint(x: 0, y: 10)
let observable = IndefiniteObservable<CGPoint> { observer in
let observable = ValueObservable<CGPoint> { observer in
observer.next(value)
return noopUnsubscription
}
Expand All @@ -158,7 +158,7 @@ class ObservableTests: XCTestCase {

func testFilteringValues() {
let value = CGPoint(x: 0, y: 10)
let observable = IndefiniteObservable<(Bool, CGPoint)> { observer in
let observable = ValueObservable<(Bool, CGPoint)> { observer in
observer.next(false, value)
observer.next(true, value)
return noopUnsubscription
Expand Down Expand Up @@ -194,7 +194,7 @@ class ObservableTests: XCTestCase {
func testGeneratedValuesAreReceived() {
let generator = DeferredGenerator()

let observable = IndefiniteObservable<Int> { observer in
let observable = ValueObservable<Int> { observer in
generator.addObserver(observer)
return {
generator.removeObserver(observer)
Expand Down Expand Up @@ -223,7 +223,7 @@ class ObservableTests: XCTestCase {
func testGeneratedValuesAreNotReceivedAfterUnsubscription() {
let generator = DeferredGenerator()

let observable = IndefiniteObservable<Int> { observer in
let observable = ValueObservable<Int> { observer in
generator.addObserver(observer)
return {
generator.removeObserver(observer)
Expand All @@ -250,11 +250,11 @@ class ObservableTests: XCTestCase {
}

func testGeneratedValuesAreNotReceivedAfterUnsubscriptionOrder2() {
weak var weakObservable: IndefiniteObservable<Int>?
weak var weakObservable: ValueObservable<Int>?
autoreleasepool {
let generator = DeferredGenerator()

let observable = IndefiniteObservable<Int> { observer in
let observable = ValueObservable<Int> { observer in
generator.addObserver(observer)
return {
generator.removeObserver(observer)
Expand Down
Loading

0 comments on commit 8c30e9a

Please sign in to comment.