From 23ab8056420d62c0c7034e340e8e338cb66ee842 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 26 Jan 2017 12:16:42 -0500 Subject: [PATCH] Introduce MotionObservableConvertible. Summary: This type makes it possible to create meta-types that have streams. By extending MotionObservableConvertible with operators, we can use operators on meta-types. We can also accept meta-types anywhere that we'd normally accept a MotionObservable. This change sets the foundation for dramatically simplifying the overall usage of streams. Some examples of possible changes: ``` // Before runtime.write(someProperty.stream, to: someProperty) // After runtime.write(someProperty, to: someProperty) // Before someProperty.stream.x() // After someProperty.x() ``` Depends on D2562. Depends on D2561. Reviewers: chuga, O4 Material Apple platform reviewers, O2 Material Motion, #material_motion Reviewed By: chuga, O4 Material Apple platform reviewers Subscribers: appsforartists, chuga Tags: #material_motion Differential Revision: http://codereview.cc/D2563 --- src/MotionObservable.swift | 27 ++++++++----------- src/MotionRuntime.swift | 4 +-- src/operators/CGPoint.swift | 6 ++--- src/operators/arithmetic.swift | 14 +++++----- src/operators/debugging.swift | 4 +-- src/operators/distance.swift | 8 +++--- src/operators/foundation/_filter.swift | 4 +-- src/operators/foundation/_map.swift | 4 +-- src/operators/foundation/_nextOperator.swift | 6 ++--- src/operators/gestures/centroid.swift | 4 +-- .../gestures/onRecognitionState.swift | 6 ++--- src/operators/gestures/rotated.swift | 9 ++++--- src/operators/gestures/scaled.swift | 18 +++++++------ src/operators/gestures/translated.swift | 10 ++++--- src/operators/gestures/velocity.swift | 12 ++++----- src/operators/mapRange.swift | 4 +-- src/operators/mapTo.swift | 4 +-- src/operators/multicast.swift | 4 +-- src/operators/read.swift | 8 +++--- src/operators/rewrite.swift | 4 +-- src/operators/threshold.swift | 10 +++---- src/operators/toggled.swift | 8 +++--- src/operators/transitions/destinations.swift | 4 +-- src/operators/valve.swift | 12 ++++----- 24 files changed, 97 insertions(+), 97 deletions(-) diff --git a/src/MotionObservable.swift b/src/MotionObservable.swift index 9f5e321..81cd04d 100644 --- a/src/MotionObservable.swift +++ b/src/MotionObservable.swift @@ -38,7 +38,7 @@ public typealias CoreAnimationChannel = (CoreAnimationChannelEvent) -> Void Throughout this documentation we will treat the words "observable" and "stream" as synonyms. */ -public final class MotionObservable: IndefiniteObservable>, ExtendableMotionObservable { +public final class MotionObservable: IndefiniteObservable> { /** Sugar for subscribing a MotionObserver. */ public func subscribe(next: @escaping NextChannel, state: @escaping StateChannel, @@ -89,21 +89,16 @@ public final class MotionObserver: Observer { public let coreAnimation: CoreAnimationChannel } -/** - This type is used for extending MotionObservable using generics. - - This is required to be able to do extensions where T == some value, such as CGPoint. See - https://twitter.com/dgregor79/status/646167048645554176 for discussion of what appears to be a - bug in swift. - */ -public protocol ExtendableMotionObservable { +/** A MotionObservableConvertible has a canonical MotionObservable that it can return. */ +public protocol MotionObservableConvertible { associatedtype T - /** - We define this only so that T can be inferred by the compiler so that we don't have to - introduce a new generic type such as Value in the associatedtype here. - */ - func subscribe(next: @escaping NextChannel, - state: @escaping StateChannel, - coreAnimation: @escaping CoreAnimationChannel) -> Subscription + /** Returns the canonical MotionObservable for this object. */ + func asStream() -> MotionObservable +} + +extension MotionObservable: MotionObservableConvertible { + public func asStream() -> MotionObservable { + return self + } } diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift index 55ff7f3..52fd4cf 100644 --- a/src/MotionRuntime.swift +++ b/src/MotionRuntime.swift @@ -42,9 +42,9 @@ public class MotionRuntime { } /** Subscribes to the stream, writes its output to the given property, and observes its state. */ - public func write(_ stream: O, to property: ReactiveProperty) where O.T == T { + public func write(_ stream: O, to property: ReactiveProperty) where O.T == T { let token = NSUUID().uuidString - subscriptions.append(stream.subscribe(next: property.setValue, state: { [weak self] state in + subscriptions.append(stream.asStream().subscribe(next: property.setValue, state: { [weak self] state in property.state(state) guard let strongSelf = self else { return } diff --git a/src/operators/CGPoint.swift b/src/operators/CGPoint.swift index a43780b..f00220e 100644 --- a/src/operators/CGPoint.swift +++ b/src/operators/CGPoint.swift @@ -14,11 +14,11 @@ limitations under the License. */ -extension ExtendableMotionObservable where T == CGPoint { +extension MotionObservableConvertible where T == CGPoint { /** Extract the x value from a CGPoint. */ - public func x() -> MotionObservable { return _map { $0.x } } + public func x() -> MotionObservable { return asStream()._map { $0.x } } /** Extract the y value from a CGPoint. */ - public func y() -> MotionObservable { return _map { $0.y } } + public func y() -> MotionObservable { return asStream()._map { $0.y } } } diff --git a/src/operators/arithmetic.swift b/src/operators/arithmetic.swift index 21b2541..89acd95 100644 --- a/src/operators/arithmetic.swift +++ b/src/operators/arithmetic.swift @@ -16,26 +16,26 @@ import Foundation -extension ExtendableMotionObservable where T == CGFloat { +extension MotionObservableConvertible where T == CGFloat { /** Emits the incoming value + amount. */ - public func offset(by amount: CGFloat) -> MotionObservable { return _map { $0 + amount } } + public func offset(by amount: CGFloat) -> MotionObservable { return asStream()._map { $0 + amount } } /** Emits the incoming value * amount. */ - public func scaled(by amount: CGFloat) -> MotionObservable { return _map { $0 * amount } } + public func scaled(by amount: CGFloat) -> MotionObservable { return asStream()._map { $0 * amount } } /** Emits the incoming value / amount. */ - public func normalized(by amount: CGFloat) -> MotionObservable { return _map { $0 / amount } } + public func normalized(by amount: CGFloat) -> MotionObservable { return asStream()._map { $0 / amount } } /** Subtract the incoming value from the provided value. */ - public func subtracted(from value: CGFloat) -> MotionObservable { return _map { value - $0 } } + public func subtracted(from value: CGFloat) -> MotionObservable { return asStream()._map { value - $0 } } } -extension ExtendableMotionObservable where T == CGPoint { +extension MotionObservableConvertible where T == CGPoint { /** Emits the incoming value / amount. */ public func normalized(by amount: CGSize) -> MotionObservable { - return _map { + return asStream()._map { return CGPoint(x: $0.x / amount.width, y: $0.y / amount.height) } diff --git a/src/operators/debugging.swift b/src/operators/debugging.swift index 0351ec8..c0c478a 100644 --- a/src/operators/debugging.swift +++ b/src/operators/debugging.swift @@ -16,11 +16,11 @@ import Foundation -extension ExtendableMotionObservable { +extension MotionObservableConvertible { /** Writes any incoming value to the console and then passes the value on. */ public func log(_ context: String? = nil) -> MotionObservable { - return _nextOperator({ value, next in + return asStream()._nextOperator({ value, next in if let context = context { print(context, value) } else { diff --git a/src/operators/distance.swift b/src/operators/distance.swift index c723564..1f4cf96 100644 --- a/src/operators/distance.swift +++ b/src/operators/distance.swift @@ -16,21 +16,21 @@ import Foundation -extension ExtendableMotionObservable where T == CGFloat { +extension MotionObservableConvertible where T == CGFloat { /** Emits the distance between the incoming value and the location. */ public func distance(from location: CGFloat) -> MotionObservable { - return _map { + return asStream()._map { fabs($0 - location) } } } -extension ExtendableMotionObservable where T == CGPoint { +extension MotionObservableConvertible where T == CGPoint { /** Emits the distance between the incoming value and the location. */ public func distance(from location: CGPoint) -> MotionObservable { - return _map { + return asStream()._map { let xDelta = $0.x - location.x let yDelta = $0.y - location.y return sqrt(xDelta * xDelta + yDelta * yDelta) diff --git a/src/operators/foundation/_filter.swift b/src/operators/foundation/_filter.swift index eb7d7c7..2fed0ac 100644 --- a/src/operators/foundation/_filter.swift +++ b/src/operators/foundation/_filter.swift @@ -16,11 +16,11 @@ import Foundation -extension ExtendableMotionObservable { +extension MotionObservableConvertible { /** Only emit those items from an Observable that pass a test. */ public func _filter(_ predicate: @escaping (T) -> Bool) -> MotionObservable { - return _nextOperator { value, next in + return asStream()._nextOperator { value, next in if predicate(value) { next(value) } diff --git a/src/operators/foundation/_map.swift b/src/operators/foundation/_map.swift index d62fce4..2125e8f 100644 --- a/src/operators/foundation/_map.swift +++ b/src/operators/foundation/_map.swift @@ -16,11 +16,11 @@ import Foundation -extension ExtendableMotionObservable { +extension MotionObservableConvertible { /** Transform the items emitted by an Observable by applying a function to each item. */ func _map(_ transform: @escaping (T) -> U) -> MotionObservable { - return _nextOperator({ value, next in + return asStream()._nextOperator({ value, next in next(transform(value)) }, coreAnimation: { event, coreAnimation in diff --git a/src/operators/foundation/_nextOperator.swift b/src/operators/foundation/_nextOperator.swift index a96d2ba..f0f6f19 100644 --- a/src/operators/foundation/_nextOperator.swift +++ b/src/operators/foundation/_nextOperator.swift @@ -16,7 +16,7 @@ import Foundation -extension ExtendableMotionObservable { +extension MotionObservableConvertible { /** A light-weight operator builder. @@ -26,7 +26,7 @@ extension ExtendableMotionObservable { */ func _nextOperator(_ operation: @escaping (T, (U) -> Void) -> Void) -> MotionObservable { return MotionObservable { observer in - return self.subscribe(next: { + return self.asStream().subscribe(next: { return operation($0, observer.next) }, state: observer.state, coreAnimation: { _ in assertionFailure("Core animation is not supported by this operator.") @@ -43,7 +43,7 @@ extension ExtendableMotionObservable { */ func _nextOperator(_ operation: @escaping (T, (U) -> Void) -> Void, coreAnimation: @escaping (CoreAnimationChannelEvent, CoreAnimationChannel) -> Void) -> MotionObservable { return MotionObservable { observer in - return self.subscribe(next: { + return self.asStream().subscribe(next: { return operation($0, observer.next) }, state: observer.state, coreAnimation: { return coreAnimation($0, observer.coreAnimation) diff --git a/src/operators/gestures/centroid.swift b/src/operators/gestures/centroid.swift index bcb2f76..23025cf 100644 --- a/src/operators/gestures/centroid.swift +++ b/src/operators/gestures/centroid.swift @@ -16,11 +16,11 @@ import Foundation -extension ExtendableMotionObservable where T: UIGestureRecognizer { +extension MotionObservableConvertible where T: UIGestureRecognizer { /** Extract centroid from the incoming gesture recognizer. */ public func centroid(in view: UIView) -> MotionObservable { - return _map { value in + return asStream()._map { value in value.location(in: view) } } diff --git a/src/operators/gestures/onRecognitionState.swift b/src/operators/gestures/onRecognitionState.swift index 477437a..c38be55 100644 --- a/src/operators/gestures/onRecognitionState.swift +++ b/src/operators/gestures/onRecognitionState.swift @@ -16,18 +16,18 @@ import Foundation -extension ExtendableMotionObservable where T: UIGestureRecognizer { +extension MotionObservableConvertible where T: UIGestureRecognizer { /** Only forwards the gesture recognizer if its state matches the provided value. */ public func onRecognitionState(_ state: UIGestureRecognizerState) -> MotionObservable { - return _filter { value in + return asStream()._filter { value in return value.state == state } } /** Only forwards the gesture recognizer if its state matches any of the provided values. */ public func onRecognitionStates(_ states: [UIGestureRecognizerState]) -> MotionObservable { - return _filter { value in + return asStream()._filter { value in return states.contains(value.state) } } diff --git a/src/operators/gestures/rotated.swift b/src/operators/gestures/rotated.swift index 46a7b03..8660d48 100644 --- a/src/operators/gestures/rotated.swift +++ b/src/operators/gestures/rotated.swift @@ -16,17 +16,18 @@ import Foundation -extension ExtendableMotionObservable where T: UIRotationGestureRecognizer { +extension MotionObservableConvertible where T: UIRotationGestureRecognizer { /** Adds the current translation to the initial position and emits the result while the gesture recognizer is active. */ - func rotated(from initialRotation: MotionObservable) -> MotionObservable { + func rotated(from initialRotation: O) -> MotionObservable where O.T == CGFloat { + let initialRotationStream = initialRotation.asStream() var cachedInitialRotation: CGFloat? - return _nextOperator { value, next in + return asStream()._nextOperator { value, next in if value.state == .began || (value.state == .changed && cachedInitialRotation == nil) { - cachedInitialRotation = initialRotation.read() + cachedInitialRotation = initialRotationStream.read() } else if value.state != .began && value.state != .changed { cachedInitialRotation = nil } diff --git a/src/operators/gestures/scaled.swift b/src/operators/gestures/scaled.swift index 3427088..983c8a6 100644 --- a/src/operators/gestures/scaled.swift +++ b/src/operators/gestures/scaled.swift @@ -16,21 +16,23 @@ import Foundation -extension ExtendableMotionObservable where T: UIPinchGestureRecognizer { +extension MotionObservableConvertible where T: UIPinchGestureRecognizer { /** Multiplies the current scale by the initial scale and emits the result while the gesture recognizer is active. */ - func scaled(from initialScale: MotionObservable) -> MotionObservable { - var cachedInitialScale: CGFloat? - return _nextOperator { value, next in - if value.state == .began || (value.state == .changed && cachedInitialScale == nil) { - cachedInitialScale = initialScale.read() + func scaled(from initialScale: O) -> MotionObservable where O.T == CGFloat { + let initialScaleStream = initialScale.asStream() + var initialScale: CGFloat? + return asStream()._nextOperator { value, next in + if value.state == .began || (value.state == .changed && initialScale == nil) { + initialScale = initialScaleStream.read() + } else if value.state != .began && value.state != .changed { - cachedInitialScale = nil + initialScale = nil } - if let cachedInitialScale = cachedInitialScale { + if let cachedInitialScale = initialScale { let scale = value.scale next(cachedInitialScale * scale) } diff --git a/src/operators/gestures/translated.swift b/src/operators/gestures/translated.swift index 5d8175e..2cdbe4d 100644 --- a/src/operators/gestures/translated.swift +++ b/src/operators/gestures/translated.swift @@ -16,17 +16,19 @@ import Foundation -extension ExtendableMotionObservable where T: UIPanGestureRecognizer { +extension MotionObservableConvertible where T: UIPanGestureRecognizer { /** Adds the current translation to the initial position and emits the result while the gesture recognizer is active. */ - func translated(from initialPosition: MotionObservable, in view: UIView) -> MotionObservable { + func translated(from initialPosition: O, in view: UIView) -> MotionObservable where O.T == CGPoint { + let initialPositionStream = initialPosition.asStream() var cachedInitialPosition: CGPoint? - return _nextOperator { value, next in + return asStream()._nextOperator { value, next in if value.state == .began || (value.state == .changed && cachedInitialPosition == nil) { - cachedInitialPosition = initialPosition.read() + cachedInitialPosition = initialPositionStream.read() + } else if value.state != .began && value.state != .changed { cachedInitialPosition = nil } diff --git a/src/operators/gestures/velocity.swift b/src/operators/gestures/velocity.swift index 96eced4..530a5a4 100644 --- a/src/operators/gestures/velocity.swift +++ b/src/operators/gestures/velocity.swift @@ -16,28 +16,28 @@ import Foundation -extension ExtendableMotionObservable where T: UIPanGestureRecognizer { +extension MotionObservableConvertible where T: UIPanGestureRecognizer { /** Extract translational velocity from the incoming pan gesture recognizer. */ public func velocity(in view: UIView) -> MotionObservable { - return _map { value in + return asStream()._map { value in value.velocity(in: view) } } } -extension ExtendableMotionObservable where T: UIRotationGestureRecognizer { +extension MotionObservableConvertible where T: UIRotationGestureRecognizer { /** Extract rotational velocity from the incoming rotation gesture recognizer. */ public func velocity() -> MotionObservable { - return _map { value in value.velocity } + return asStream()._map { value in value.velocity } } } -extension ExtendableMotionObservable where T: UIPinchGestureRecognizer { +extension MotionObservableConvertible where T: UIPinchGestureRecognizer { /** Extract scale velocity from the incoming pinch gesture recognizer. */ public func velocity() -> MotionObservable { - return _map { value in value.velocity } + return asStream()._map { value in value.velocity } } } diff --git a/src/operators/mapRange.swift b/src/operators/mapRange.swift index 3f1a65c..a14cd21 100644 --- a/src/operators/mapRange.swift +++ b/src/operators/mapRange.swift @@ -16,7 +16,7 @@ import Foundation -extension ExtendableMotionObservable where T: Subtractable, T: Lerpable { +extension MotionObservableConvertible where T: Subtractable, T: Lerpable { /** Linearly interpolate the incoming value along the given range to the destination range. */ public func mapRange( @@ -25,7 +25,7 @@ extension ExtendableMotionObservable where T: Subtractable, T: Lerpable { destinationStart: U, destinationEnd: U) -> MotionObservable where U: Lerpable, U: Subtractable, U: Addable { - return _map { + return asStream()._map { let position = $0 - rangeStart let vector = rangeEnd - rangeStart diff --git a/src/operators/mapTo.swift b/src/operators/mapTo.swift index 701964a..8a896ce 100644 --- a/src/operators/mapTo.swift +++ b/src/operators/mapTo.swift @@ -16,10 +16,10 @@ import Foundation -extension ExtendableMotionObservable { +extension MotionObservableConvertible { /** Emit a constant value each time this operator receives a value. */ public func mapTo(_ value: U) -> MotionObservable { - return _map { _ in value } + return asStream()._map { _ in value } } } diff --git a/src/operators/multicast.swift b/src/operators/multicast.swift index dca1801..912e32e 100644 --- a/src/operators/multicast.swift +++ b/src/operators/multicast.swift @@ -16,7 +16,7 @@ import IndefiniteObservable -extension ExtendableMotionObservable { +extension MotionObservableConvertible { /** Turns a stream into a multicast stream. @@ -33,7 +33,7 @@ extension ExtendableMotionObservable { var lastCoreAnimationEvent: CoreAnimationChannelEvent? let subscribe = { - subscription = self.subscribe(next: { value in + subscription = self.asStream().subscribe(next: { value in lastValue = value for observer in observers { observer.next(value) diff --git a/src/operators/read.swift b/src/operators/read.swift index e687382..e9bd933 100644 --- a/src/operators/read.swift +++ b/src/operators/read.swift @@ -16,16 +16,16 @@ import Foundation -extension ExtendableMotionObservable { +extension MotionObservableConvertible { /** Return the value emitted by the stream on subscription. Will throw an assertion if no value was emitted. */ - public func read() -> T { + public func read() -> T? { var value: T? - subscribe(next: { value = $0 }, state: { _ in }, coreAnimation: { _ in }) - return value! + asStream().subscribe(next: { value = $0 }, state: { _ in }, coreAnimation: { _ in }) + return value } } diff --git a/src/operators/rewrite.swift b/src/operators/rewrite.swift index c9cef16..d3d6649 100644 --- a/src/operators/rewrite.swift +++ b/src/operators/rewrite.swift @@ -16,11 +16,11 @@ import Foundation -extension ExtendableMotionObservable where T: Hashable { +extension MotionObservableConvertible where T: Hashable { /** Emits the mapped value for each incoming value, if one exists, otherwise emits nothing. */ public func rewrite(_ values: [T: U]) -> MotionObservable { - return _nextOperator { value, next in + return asStream()._nextOperator { value, next in if let mappedValue = values[value] { next(mappedValue) } diff --git a/src/operators/threshold.swift b/src/operators/threshold.swift index 32e6a58..c798ca4 100644 --- a/src/operators/threshold.swift +++ b/src/operators/threshold.swift @@ -16,7 +16,7 @@ import Foundation -extension ExtendableMotionObservable where T: Comparable { +extension MotionObservableConvertible where T: Comparable { /** Emit a value based on the incoming value's position around a threshold. @@ -31,7 +31,7 @@ extension ExtendableMotionObservable where T: Comparable { whenEqual equal: U, whenBelow below: U, whenAbove above: U) -> MotionObservable { - return _map { + return asStream()._map { if $0 < threshold { return below } @@ -56,7 +56,7 @@ extension ExtendableMotionObservable where T: Comparable { whenWithin within: U, whenBelow below: U, whenAbove above: U) -> MotionObservable { - return _map { + return asStream()._map { if $0 < min { return below } @@ -69,14 +69,14 @@ extension ExtendableMotionObservable where T: Comparable { /** Emits either the incoming value or the provided maxValue, whichever is smaller. */ public func max(_ maxValue: T) -> MotionObservable { - return _map { + return asStream()._map { return Swift.min($0, maxValue) } } /** Emits either the incoming value or the provided minValue, whichever is larger. */ public func min(_ minValue: T) -> MotionObservable { - return _map { + return asStream()._map { return Swift.max($0, minValue) } } diff --git a/src/operators/toggled.swift b/src/operators/toggled.swift index 0fb1e54..d23da6f 100644 --- a/src/operators/toggled.swift +++ b/src/operators/toggled.swift @@ -16,7 +16,7 @@ import IndefiniteObservable -extension ExtendableMotionObservable { +extension MotionObservableConvertible { /** Toggled emits values from this stream or the provided one, preferring the provided stream while @@ -58,9 +58,9 @@ extension ExtendableMotionObservable { } if state == .atRest && originalStreamSubscription == nil { - originalStreamSubscription = self.subscribe(next: observer.next, - state: observer.state, - coreAnimation: observer.coreAnimation) + originalStreamSubscription = self.asStream().subscribe(next: observer.next, + state: observer.state, + coreAnimation: observer.coreAnimation) } if state == .active { diff --git a/src/operators/transitions/destinations.swift b/src/operators/transitions/destinations.swift index 86181b4..53acb57 100644 --- a/src/operators/transitions/destinations.swift +++ b/src/operators/transitions/destinations.swift @@ -16,10 +16,10 @@ import Foundation -extension ExtendableMotionObservable where T == Transition.Direction { +extension MotionObservableConvertible where T == Transition.Direction { /** Emits either the back or fore value when a new direction is received, . */ public func destinations(back: U, fore: U) -> MotionObservable { - return _map { direction in direction == .forward ? fore : back } + return asStream()._map { direction in direction == .forward ? fore : back } } } diff --git a/src/operators/valve.swift b/src/operators/valve.swift index 6267f5f..0e5fc3c 100644 --- a/src/operators/valve.swift +++ b/src/operators/valve.swift @@ -16,7 +16,7 @@ import IndefiniteObservable -extension ExtendableMotionObservable { +extension MotionObservableConvertible { /** A valve creates control flow for a stream. @@ -24,19 +24,19 @@ extension ExtendableMotionObservable { The upstream will be subscribed to when valveStream emits true, and the subscription terminated when the valveStream emits false. */ - public func valve(_ valveStream: O) -> MotionObservable where O.T == Bool { + public func valve(_ observable: O) -> MotionObservable where O.T == Bool { return MotionObservable { observer in var valveSubscription: Subscription? var upstreamSubscription: Subscription? var connectUpstream = { - upstreamSubscription = self.subscribe(next: observer.next, - state: observer.state, - coreAnimation: observer.coreAnimation) + upstreamSubscription = self.asStream().subscribe(next: observer.next, + state: observer.state, + coreAnimation: observer.coreAnimation) } connectUpstream() - valveSubscription = valveStream.subscribe(next: { value in + valveSubscription = observable.asStream().subscribe(next: { value in let shouldOpen = value if shouldOpen && upstreamSubscription == nil {