From f90e15b7d62b7f7102e7827a5a850bcbfa7a0451 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 19 Apr 2017 16:14:27 -0400 Subject: [PATCH 01/60] Add Togglable conformity to Rotatable and Scalable. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3076 --- src/interactions/Rotatable.swift | 2 +- src/interactions/Scalable.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interactions/Rotatable.swift b/src/interactions/Rotatable.swift index 6f06f13..54c64d2 100644 --- a/src/interactions/Rotatable.swift +++ b/src/interactions/Rotatable.swift @@ -30,7 +30,7 @@ import UIKit CGFloat constraints may be applied to this interaction. */ -public final class Rotatable: Gesturable, Interaction, Stateful { +public final class Rotatable: Gesturable, Interaction, Togglable, Stateful { public func add(to view: UIView, withRuntime runtime: MotionRuntime, constraints applyConstraints: ConstraintApplicator? = nil) { diff --git a/src/interactions/Scalable.swift b/src/interactions/Scalable.swift index d208072..1df2bd1 100644 --- a/src/interactions/Scalable.swift +++ b/src/interactions/Scalable.swift @@ -30,7 +30,7 @@ import UIKit CGFloat constraints may be applied to this interaction. */ -public final class Scalable: Gesturable, Interaction, Stateful { +public final class Scalable: Gesturable, Interaction, Togglable, Stateful { public func add(to view: UIView, withRuntime runtime: MotionRuntime, constraints applyConstraints: ConstraintApplicator? = nil) { From b31c1361ca6744c0c069db98bb6c6b89e3ffbb4b Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 19 Apr 2017 16:54:55 -0400 Subject: [PATCH 02/60] Add missing Foundation import. --- src/common/DispatchTimeIntervalToSeconds.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/DispatchTimeIntervalToSeconds.swift b/src/common/DispatchTimeIntervalToSeconds.swift index 63f4c65..f5bf24f 100644 --- a/src/common/DispatchTimeIntervalToSeconds.swift +++ b/src/common/DispatchTimeIntervalToSeconds.swift @@ -14,6 +14,8 @@ limitations under the License. */ +import Foundation + extension DispatchTimeInterval { func toSeconds() -> CGFloat { let seconds: CGFloat From 3f16a4d90fc842999fb23b3af7954c69bf6c64a2 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 19 Apr 2017 17:43:36 -0400 Subject: [PATCH 03/60] Add missing imports. --- src/common/DispatchTimeIntervalToSeconds.swift | 1 + src/operators/visualize.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/src/common/DispatchTimeIntervalToSeconds.swift b/src/common/DispatchTimeIntervalToSeconds.swift index f5bf24f..2cce522 100644 --- a/src/common/DispatchTimeIntervalToSeconds.swift +++ b/src/common/DispatchTimeIntervalToSeconds.swift @@ -15,6 +15,7 @@ */ import Foundation +import CoreGraphics extension DispatchTimeInterval { func toSeconds() -> CGFloat { diff --git a/src/operators/visualize.swift b/src/operators/visualize.swift index 39936a5..835cdce 100644 --- a/src/operators/visualize.swift +++ b/src/operators/visualize.swift @@ -15,6 +15,7 @@ */ import Foundation +import UIKit extension MotionObservableConvertible { From e2ad598ae466e4dfe034be751796fd96a82cb37b Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 18 Apr 2017 14:20:10 -0400 Subject: [PATCH 04/60] Deprecate transitionController.dismisser and move the APIs into TransitionController. Summary: Also generally consolidating transition gesture recognizers into a single entity so that it's easier to add gesture recognizers to the transition. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3068 --- examples/ContextualTransitionExample.swift | 5 +- ...InteractivePushBackTransitionExample.swift | 4 +- src/transitions/TransitionContext.swift | 11 +--- src/transitions/TransitionController.swift | 63 ++++++++++++++++--- src/transitions/ViewControllerDismisser.swift | 15 +++-- 5 files changed, 72 insertions(+), 26 deletions(-) diff --git a/examples/ContextualTransitionExample.swift b/examples/ContextualTransitionExample.swift index 0767da9..eb26aa6 100644 --- a/examples/ContextualTransitionExample.swift +++ b/examples/ContextualTransitionExample.swift @@ -202,11 +202,10 @@ class PhotoAlbumViewController: UIViewController, UICollectionViewDataSource, UI view.addSubview(collectionView) - let dismisser = transitionController.dismisser - dismisser.disableSimultaneousRecognition(of: collectionView.panGestureRecognizer) + transitionController.disableSimultaneousRecognition(of: collectionView.panGestureRecognizer) for gesture in [UIPanGestureRecognizer(), UIPinchGestureRecognizer(), UIRotationGestureRecognizer()] { - dismisser.dismissWhenGestureRecognizerBegins(gesture) + transitionController.dismissWhenGestureRecognizerBegins(gesture) view.addGestureRecognizer(gesture) } } diff --git a/examples/InteractivePushBackTransitionExample.swift b/examples/InteractivePushBackTransitionExample.swift index 4ffaa6b..d21c66c 100644 --- a/examples/InteractivePushBackTransitionExample.swift +++ b/examples/InteractivePushBackTransitionExample.swift @@ -59,8 +59,8 @@ private class ModalViewController: UIViewController, UIGestureRecognizerDelegate view.addSubview(scrollView) let pan = UIPanGestureRecognizer() - pan.delegate = transitionController.dismisser.topEdgeDismisserDelegate(for: scrollView) - transitionController.dismisser.dismissWhenGestureRecognizerBegins(pan) + pan.delegate = transitionController.topEdgeDismisserDelegate(for: scrollView) + transitionController.dismissWhenGestureRecognizerBegins(pan) scrollView.panGestureRecognizer.require(toFail: pan) view.addGestureRecognizer(pan) } diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index 6917333..8664d59 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -85,11 +85,7 @@ public final class TransitionContext: NSObject { public let fore: UIViewController /** The set of gesture recognizers associated with this transition. */ - public var gestureRecognizers: Set { - get { - return dismisser.gestureRecognizers - } - } + public let gestureRecognizers: Set /** The runtime to which motion should be registered. */ fileprivate var runtime: MotionRuntime! @@ -100,12 +96,12 @@ public final class TransitionContext: NSObject { direction: TransitionDirection, back: UIViewController, fore: UIViewController, - dismisser: ViewControllerDismisser) { + gestureRecognizers: Set) { self.direction = createProperty("Transition.direction", withInitialValue: direction) self.initialDirection = direction self.back = back self.fore = fore - self.dismisser = dismisser + self.gestureRecognizers = gestureRecognizers self.window = TransitionTimeWindow(duration: TransitionContext.defaultDuration) // TODO: Create a Timeline. @@ -118,7 +114,6 @@ public final class TransitionContext: NSObject { fileprivate let initialDirection: TransitionDirection fileprivate var transition: Transition! fileprivate var context: UIViewControllerContextTransitioning! - fileprivate let dismisser: ViewControllerDismisser fileprivate var didRegisterTerminator = false } diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index d93b679..51906c9 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -69,14 +69,44 @@ public final class TransitionController { } /** - Gesture recognizers associated with a view controller dismisser will cause the associated view - controller to be dismissed when the gesture recognizers begin. + Start a dismiss transition when the given gesture recognizer enters its began or recognized + state. - Provided gesture recognizers will also be made available to the Transition instance via the - TransitionContext's gestureRecognizers property. + The provided gesture recognizer will be made available to the transition instance via the + TransitionContext's `gestureRecognizers` property. */ - public var dismisser: ViewControllerDismisser { - return _transitioningDelegate.dismisser + public func dismissWhenGestureRecognizerBegins(_ gestureRecognizer: UIGestureRecognizer) { + _transitioningDelegate.dismisser.dismissWhenGestureRecognizerBegins(gestureRecognizer) + } + + /** + Will not allow the provided gesture recognizer to recognize simultaneously with other gesture + recognizers. + + This method assumes that the provided gesture recognizer's delegate has been assigned to the + transition controller's gesture delegate. + */ + public func disableSimultaneousRecognition(of gestureRecognizer: UIGestureRecognizer) { + _transitioningDelegate.dismisser.disableSimultaneousRecognition(of: gestureRecognizer) + } + + /** + Returns a gesture recognizer delegate that will allow the gesture recognizer to begin only if the + provided scroll view is scrolled to the top of its content. + + The returned delegate implements gestureRecognizerShouldBegin. + */ + public func topEdgeDismisserDelegate(for scrollView: UIScrollView) -> UIGestureRecognizerDelegate { + return _transitioningDelegate.dismisser.topEdgeDismisserDelegate(for: scrollView) + } + + /** + The set of gesture recognizers that will be provided to the transition via the TransitionContext + instance. + */ + public var gestureRecognizers: Set { + set { _transitioningDelegate.gestureDelegate.gestureRecognizers = newValue } + get { return _transitioningDelegate.gestureDelegate.gestureRecognizers } } /** @@ -89,10 +119,25 @@ public final class TransitionController { return _transitioningDelegate } + /** + The gesture recognizer delegate managed by this controller. + */ + public var gestureDelegate: UIGestureRecognizerDelegate { + return _transitioningDelegate.gestureDelegate + } + init(viewController: UIViewController) { _transitioningDelegate = TransitioningDelegate(viewController: viewController) } + /** + Deprecated. Please use methods directly on the transitionController instead. + */ + @available(*, deprecated, message: "Please use methods directly on the transitionController instead.") + public var dismisser: ViewControllerDismisser { + return _transitioningDelegate.dismisser + } + fileprivate let _transitioningDelegate: TransitioningDelegate } @@ -100,7 +145,7 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni init(viewController: UIViewController) { self.associatedViewController = viewController - self.dismisser = ViewControllerDismisser() + self.dismisser = ViewControllerDismisser(gestureDelegate: self.gestureDelegate) super.init() @@ -109,7 +154,9 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni var ctx: TransitionContext? var transitionType: Transition.Type? + let dismisser: ViewControllerDismisser + let gestureDelegate = GestureDelegate() weak var associatedViewController: UIViewController? @@ -133,7 +180,7 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni direction: direction, back: back, fore: fore, - dismisser: dismisser) + gestureRecognizers: gestureDelegate.gestureRecognizers) ctx?.delegate = self } } diff --git a/src/transitions/ViewControllerDismisser.swift b/src/transitions/ViewControllerDismisser.swift index 9a811e4..65b85fa 100644 --- a/src/transitions/ViewControllerDismisser.swift +++ b/src/transitions/ViewControllerDismisser.swift @@ -74,10 +74,15 @@ public final class ViewControllerDismisser { weak var delegate: ViewControllerDismisserDelegate? public var gestureRecognizers: Set { - return gestureDelegate.gestureRecognizers + set { gestureDelegate.gestureRecognizers = newValue } + get { return gestureDelegate.gestureRecognizers } } - private var gestureDelegate = GestureDelegate() + init(gestureDelegate: GestureDelegate) { + self.gestureDelegate = gestureDelegate + } + + private var gestureDelegate: GestureDelegate private var scrollViewTopEdgeDismisserDelegates: [ScrollViewTopEdgeDismisserDelegate] = [] } @@ -93,9 +98,9 @@ private final class ScrollViewTopEdgeDismisserDelegate: NSObject, UIGestureRecog weak var scrollView: UIScrollView? } -private final class GestureDelegate: NSObject, UIGestureRecognizerDelegate { - fileprivate var gestureRecognizers = Set() - fileprivate var soloGestureRecognizers = Set() +final class GestureDelegate: NSObject, UIGestureRecognizerDelegate { + var gestureRecognizers = Set() + var soloGestureRecognizers = Set() public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if soloGestureRecognizers.contains(gestureRecognizer) || soloGestureRecognizers.contains(otherGestureRecognizer) { From 3498a0f8e7d177e21e89fc2934c371423e79f1f2 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 17 Apr 2017 17:02:24 -0400 Subject: [PATCH 05/60] Add foreAlignmentEdge property to TransitionController. Summary: If a view controller has a non-zero preferredContentSize, then the foreAlignmentEdge will be used to align the view to either the center of the screen, if nil, or to the specified edge (one of minX, minY, maxX, or maxY). If preferredContentSize is zero, then the transition's `finalFrame` value will be used instead - this is generally the containerView's bounds. This API is useful for building modal dialogs and sliding drawers that are presented over the current context with `modalPresentationStyle = .overCurrentContext`. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Subscribers: markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3067 --- examples/ModalDialogExample.swift | 13 ++----- src/transitions/TransitionContext.swift | 40 ++++++++++++++++++++-- src/transitions/TransitionController.swift | 9 ++++- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/examples/ModalDialogExample.swift b/examples/ModalDialogExample.swift index ca44606..4d5aeaa 100644 --- a/examples/ModalDialogExample.swift +++ b/examples/ModalDialogExample.swift @@ -46,7 +46,7 @@ class ModalDialogViewController: UIViewController { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) transitionController.transitionType = ModalDialogTransition.self - + preferredContentSize = .init(width: 200, height: 200) modalPresentationStyle = .overCurrentContext } @@ -64,8 +64,6 @@ class ModalDialogViewController: UIViewController { view.layer.shadowRadius = 5 view.layer.shadowOpacity = 1 view.layer.shadowOffset = .init(width: 0, height: 2) - - preferredContentSize = .init(width: 200, height: 200) } } @@ -74,15 +72,10 @@ class ModalDialogTransition: SelfDismissingTransition { required init() {} func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] { - let size = ctx.fore.preferredContentSize - - if ctx.direction == .forward { - ctx.fore.view.bounds = CGRect(origin: .zero, size: size) - } - + let size = ctx.fore.view.frame.size let bounds = ctx.containerView().bounds let backPosition = CGPoint(x: bounds.midX, y: bounds.maxY + size.height * 3 / 4) - let forePosition = CGPoint(x: bounds.midX, y: bounds.midY) + let forePosition = ctx.fore.view.layer.position let reactiveForeLayer = runtime.get(ctx.fore.view.layer) let position = reactiveForeLayer.position diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index 8664d59..5c63e68 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -84,6 +84,8 @@ public final class TransitionContext: NSObject { */ public let fore: UIViewController + public let foreAlignmentEdge: CGRectEdge? + /** The set of gesture recognizers associated with this transition. */ public let gestureRecognizers: Set @@ -96,12 +98,14 @@ public final class TransitionContext: NSObject { direction: TransitionDirection, back: UIViewController, fore: UIViewController, - gestureRecognizers: Set) { + gestureRecognizers: Set, + foreAlignmentEdge: CGRectEdge?) { self.direction = createProperty("Transition.direction", withInitialValue: direction) self.initialDirection = direction self.back = back self.fore = fore self.gestureRecognizers = gestureRecognizers + self.foreAlignmentEdge = foreAlignmentEdge self.window = TransitionTimeWindow(duration: TransitionContext.defaultDuration) // TODO: Create a Timeline. @@ -140,6 +144,30 @@ extension TransitionContext: UIViewControllerInteractiveTransitioning { } } +private func preferredFrame(for viewController: UIViewController, + inBounds bounds: CGRect, + alignmentEdge: CGRectEdge?) -> CGRect? { + guard viewController.preferredContentSize != .zero() else { + return nil + } + + let size = viewController.preferredContentSize + let origin: CGPoint + switch alignmentEdge { + case nil: // Centered + origin = .init(x: bounds.midX - size.width / 2, y: bounds.midY - size.height / 2) + case .minXEdge?: + origin = .init(x: bounds.minX, y: bounds.midY - size.height / 2) + case .minYEdge?: + origin = .init(x: bounds.midX - size.width / 2, y: bounds.minY) + case .maxXEdge?: + origin = .init(x: bounds.maxX - size.width, y: bounds.midY - size.height / 2) + case .maxYEdge?: + origin = .init(x: bounds.midX - size.width / 2, y: bounds.maxY - size.height) + } + return .init(origin: origin, size: size) +} + extension TransitionContext { fileprivate func initiateTransition() { if let from = context.viewController(forKey: .from) { @@ -150,7 +178,15 @@ extension TransitionContext { } if let to = context.viewController(forKey: .to) { - let finalFrame = context.finalFrame(for: to) + let finalFrame: CGRect + + if let preferredFrame = preferredFrame(for: to, + inBounds: context.containerView.bounds, + alignmentEdge: (direction.value == .forward) ? foreAlignmentEdge : nil) { + finalFrame = preferredFrame + } else { + finalFrame = context.finalFrame(for: to) + } if !finalFrame.isEmpty { to.view.frame = finalFrame } diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index 51906c9..13bcc39 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -109,6 +109,11 @@ public final class TransitionController { get { return _transitioningDelegate.gestureDelegate.gestureRecognizers } } + public var foreAlignmentEdge: CGRectEdge? { + set { _transitioningDelegate.foreAlignmentEdge = newValue } + get { return _transitioningDelegate.foreAlignmentEdge } + } + /** The transitioning delegate managed by this controller. @@ -152,6 +157,7 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni self.dismisser.delegate = self } + var foreAlignmentEdge: CGRectEdge? var ctx: TransitionContext? var transitionType: Transition.Type? @@ -180,7 +186,8 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni direction: direction, back: back, fore: fore, - gestureRecognizers: gestureDelegate.gestureRecognizers) + gestureRecognizers: gestureDelegate.gestureRecognizers, + foreAlignmentEdge: foreAlignmentEdge) ctx?.delegate = self } } From 3e471897d3b9f2d401ba6d125b480bf1c621405e Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 20 Apr 2017 12:47:33 -0400 Subject: [PATCH 06/60] Fix build failure. --- src/transitions/TransitionContext.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index 5c63e68..8c22df5 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -165,7 +165,7 @@ private func preferredFrame(for viewController: UIViewController, case .maxYEdge?: origin = .init(x: bounds.midX - size.width / 2, y: bounds.maxY - size.height) } - return .init(origin: origin, size: size) + return CGRect(origin: origin, size: size) } extension TransitionContext { From d43a5f69cdcf1029273f8873f225845c62a994d1 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 21 Apr 2017 16:56:29 -0400 Subject: [PATCH 07/60] When no gesture recognizer is provided to a gestural interaction that expects one, the interaction now does nothing. Summary: Gestural interactions configured to expect a gesture recognizer will no longer connect any streams if a gesture recognizer is not provided. The `withFirstGestureIn:` now uses this new behavior when it can't find a gesture recognizer that qualifies for the interaction. Prior to this change, if the withFirstGestureIn initializer couldn't find a gesture recognizer it would default to `registerNewRecognizerToTargetView` behavior. This can cause superfluous gesture recognizers to be registered to a view. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Subscribers: markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3077 --- examples/ModalDialogExample.swift | 17 ++++++----- src/interactions/ChangeDirection.swift | 7 +++-- src/interactions/DirectlyManipulable.swift | 8 ++--- src/interactions/Draggable.swift | 11 +++++-- src/interactions/Rotatable.swift | 4 ++- src/interactions/Scalable.swift | 4 ++- src/interactions/SlopRegion.swift | 7 +++-- src/protocols/Gesturable.swift | 35 ++++++++++++---------- 8 files changed, 56 insertions(+), 37 deletions(-) diff --git a/examples/ModalDialogExample.swift b/examples/ModalDialogExample.swift index 4d5aeaa..9756f04 100644 --- a/examples/ModalDialogExample.swift +++ b/examples/ModalDialogExample.swift @@ -82,19 +82,20 @@ class ModalDialogTransition: SelfDismissingTransition { let draggable = Draggable(withFirstGestureIn: ctx.gestureRecognizers) - let gesture = runtime.get(draggable.nextGestureRecognizer) let centerY = ctx.containerView().bounds.height / 2.0 runtime.add(ChangeDirection(withVelocityOf: draggable.nextGestureRecognizer, whenNegative: .forward), to: ctx.direction) - runtime.connect(gesture - .velocityOnReleaseStream() - .y() - .thresholdRange(min: -100, max: 100) - .rewrite([.within: position.y().threshold(centerY).rewrite([.below: .forward, - .above: .backward])]), - to: ctx.direction) + if let gesture = draggable.nextGestureRecognizer { + runtime.connect(runtime.get(gesture) + .velocityOnReleaseStream() + .y() + .thresholdRange(min: -100, max: 100) + .rewrite([.within: position.y().threshold(centerY).rewrite([.below: .forward, + .above: .backward])]), + to: ctx.direction) + } let movement = TransitionSpring(back: backPosition, fore: forePosition, diff --git a/src/interactions/ChangeDirection.swift b/src/interactions/ChangeDirection.swift index 0e1dfc9..e0c6995 100644 --- a/src/interactions/ChangeDirection.swift +++ b/src/interactions/ChangeDirection.swift @@ -35,7 +35,7 @@ public final class ChangeDirection: Interaction { /** The gesture recognizer that will be observed by this interaction. */ - public let gesture: UIPanGestureRecognizer + public let gesture: UIPanGestureRecognizer? /** The minimum absolute velocity required change the transition's direction. @@ -57,7 +57,7 @@ public final class ChangeDirection: Interaction { /** - parameter minimumVelocity: The minimum absolute velocity required to change the transition's direction. */ - public init(withVelocityOf gesture: UIPanGestureRecognizer, minimumVelocity: CGFloat = 100, whenNegative: TransitionDirection = .backward, whenPositive: TransitionDirection = .backward) { + public init(withVelocityOf gesture: UIPanGestureRecognizer?, minimumVelocity: CGFloat = 100, whenNegative: TransitionDirection = .backward, whenPositive: TransitionDirection = .backward) { self.gesture = gesture self.minimumVelocity = minimumVelocity self.whenNegative = whenNegative @@ -80,6 +80,9 @@ public final class ChangeDirection: Interaction { } public func add(to direction: ReactiveProperty, withRuntime runtime: MotionRuntime, constraints axis: Axis?) { + guard let gesture = gesture else { + return + } let axis = axis ?? .y let chooseAxis: (MotionObservable) -> MotionObservable switch axis { diff --git a/src/interactions/DirectlyManipulable.swift b/src/interactions/DirectlyManipulable.swift index aa16691..72e22a3 100644 --- a/src/interactions/DirectlyManipulable.swift +++ b/src/interactions/DirectlyManipulable.swift @@ -64,8 +64,8 @@ public final class DirectlyManipulable: NSObject, Interaction, Togglable, Statef for gestureRecognizer in [draggable.nextGestureRecognizer, rotatable.nextGestureRecognizer, scalable.nextGestureRecognizer] { - if gestureRecognizer.delegate == nil { - gestureRecognizer.delegate = self + if gestureRecognizer?.delegate == nil { + gestureRecognizer?.delegate = self } } @@ -73,8 +73,8 @@ public final class DirectlyManipulable: NSObject, Interaction, Togglable, Statef runtime.connect(enabled, to: rotatable.enabled) runtime.connect(enabled, to: scalable.enabled) - let adjustsAnchorPoint = AdjustsAnchorPoint(gestureRecognizers: [rotatable.nextGestureRecognizer, - scalable.nextGestureRecognizer]) + let anchorPointRecognizers = [rotatable.nextGestureRecognizer, scalable.nextGestureRecognizer].flatMap { $0 } + let adjustsAnchorPoint = AdjustsAnchorPoint(gestureRecognizers: anchorPointRecognizers) runtime.add(adjustsAnchorPoint, to: view) aggregateState.observe(state: draggable.state, withRuntime: runtime) diff --git a/src/interactions/Draggable.swift b/src/interactions/Draggable.swift index 9338529..503a121 100644 --- a/src/interactions/Draggable.swift +++ b/src/interactions/Draggable.swift @@ -47,7 +47,9 @@ public final class Draggable: Gesturable, Interaction, T withRuntime runtime: MotionRuntime, constraints applyConstraints: ConstraintApplicator? = nil) { let reactiveView = runtime.get(view) - let gestureRecognizer = dequeueGestureRecognizer(withReactiveView: reactiveView) + guard let gestureRecognizer = dequeueGestureRecognizer(withReactiveView: reactiveView) else { + return + } let position = reactiveView.reactiveLayer.position runtime.connect(enabled, to: ReactiveProperty(initialValue: gestureRecognizer.isEnabled) { enabled in @@ -75,17 +77,20 @@ public final class Draggable: Gesturable, Interaction, T CGPoint constraints may be applied to this interaction. */ public final class DraggableFinalVelocity: Interaction { - fileprivate init(gestureRecognizer: UIPanGestureRecognizer) { + fileprivate init(gestureRecognizer: UIPanGestureRecognizer?) { self.gestureRecognizer = gestureRecognizer } public func add(to target: ReactiveProperty, withRuntime runtime: MotionRuntime, constraints applyConstraints: ConstraintApplicator? = nil) { + guard let gestureRecognizer = gestureRecognizer else { + return + } let gesture = runtime.get(gestureRecognizer) runtime.connect(gesture.velocityOnReleaseStream(in: runtime.containerView), to: target) } - let gestureRecognizer: UIPanGestureRecognizer + let gestureRecognizer: UIPanGestureRecognizer? } diff --git a/src/interactions/Rotatable.swift b/src/interactions/Rotatable.swift index 54c64d2..d40257b 100644 --- a/src/interactions/Rotatable.swift +++ b/src/interactions/Rotatable.swift @@ -35,7 +35,9 @@ public final class Rotatable: Gesturable, Interacti withRuntime runtime: MotionRuntime, constraints applyConstraints: ConstraintApplicator? = nil) { let reactiveView = runtime.get(view) - let gestureRecognizer = dequeueGestureRecognizer(withReactiveView: reactiveView) + guard let gestureRecognizer = dequeueGestureRecognizer(withReactiveView: reactiveView) else { + return + } let rotation = reactiveView.reactiveLayer.rotation runtime.connect(enabled, to: ReactiveProperty(initialValue: gestureRecognizer.isEnabled) { enabled in diff --git a/src/interactions/Scalable.swift b/src/interactions/Scalable.swift index 1df2bd1..5e4235a 100644 --- a/src/interactions/Scalable.swift +++ b/src/interactions/Scalable.swift @@ -35,7 +35,9 @@ public final class Scalable: Gesturable, Interaction, withRuntime runtime: MotionRuntime, constraints applyConstraints: ConstraintApplicator? = nil) { let reactiveView = runtime.get(view) - let gestureRecognizer = dequeueGestureRecognizer(withReactiveView: reactiveView) + guard let gestureRecognizer = dequeueGestureRecognizer(withReactiveView: reactiveView) else { + return + } let scale = reactiveView.reactiveLayer.scale runtime.connect(enabled, to: ReactiveProperty(initialValue: gestureRecognizer.isEnabled) { enabled in diff --git a/src/interactions/SlopRegion.swift b/src/interactions/SlopRegion.swift index 01da615..7544d7b 100644 --- a/src/interactions/SlopRegion.swift +++ b/src/interactions/SlopRegion.swift @@ -29,14 +29,14 @@ public final class SlopRegion: Interaction { /** The gesture recognizer that will be observed by this interaction. */ - public let gesture: UIPanGestureRecognizer + public let gesture: UIPanGestureRecognizer? /** The size of the slop region. */ public let size: CGFloat - public init(withTranslationOf gesture: UIPanGestureRecognizer, size: CGFloat) { + public init(withTranslationOf gesture: UIPanGestureRecognizer?, size: CGFloat) { self.gesture = gesture self.size = size } @@ -57,6 +57,9 @@ public final class SlopRegion: Interaction { } public func add(to direction: ReactiveProperty, withRuntime runtime: MotionRuntime, constraints axis: Axis?) { + guard let gesture = gesture else { + return + } let axis = axis ?? .y let chooseAxis: (MotionObservable) -> MotionObservable switch axis { diff --git a/src/protocols/Gesturable.swift b/src/protocols/Gesturable.swift index 5857d3f..a833712 100644 --- a/src/protocols/Gesturable.swift +++ b/src/protocols/Gesturable.swift @@ -34,11 +34,13 @@ public enum GesturableConfiguration { case registerNewRecognizerTo(UIView) /** - The interaction will make use of the provided gesture recognizer. + The interaction will make use of the provided gesture recognizer, if provided. + + If no gesture recognizer is provided then this interaction will do nothing. The interaction will not associate this gesture recognizer with any view. */ - case withExistingRecognizer(T) + case withExistingRecognizer(T?) } /** @@ -68,11 +70,7 @@ public class Gesturable { break } } - if let first = first { - self.init(.withExistingRecognizer(first)) - } else { - self.init() - } + self.init(.withExistingRecognizer(first)) } public init(_ config: GesturableConfiguration) { @@ -81,7 +79,11 @@ public class Gesturable { let initialState: MotionState switch self.config { case .withExistingRecognizer(let recognizer): - initialState = (recognizer.state == .began || recognizer.state == .changed) ? .active : .atRest + if let recognizer = recognizer { + initialState = (recognizer.state == .began || recognizer.state == .changed) ? .active : .atRest + } else { + initialState = .atRest + } default: () initialState = .atRest } @@ -95,20 +97,21 @@ public class Gesturable { This property may change after the interaction has been added to a view depending on the interaction's configuration. */ - public var nextGestureRecognizer: T { + public var nextGestureRecognizer: T? { if let nextGestureRecognizer = _nextGestureRecognizer { return nextGestureRecognizer } - let gestureRecognizer: T + let gestureRecognizer: T? switch config { case .registerNewRecognizerToTargetView: gestureRecognizer = T() case .registerNewRecognizerTo(let view): - gestureRecognizer = T() - view.addGestureRecognizer(gestureRecognizer) + let recognizer = T() + view.addGestureRecognizer(recognizer) + gestureRecognizer = recognizer case .withExistingRecognizer(let existingGestureRecognizer): gestureRecognizer = existingGestureRecognizer @@ -121,18 +124,18 @@ public class Gesturable { /** Prepares and returns the gesture recognizer that should be used to drive this interaction. */ - func dequeueGestureRecognizer(withReactiveView reactiveView: ReactiveUIView) -> T { + func dequeueGestureRecognizer(withReactiveView reactiveView: ReactiveUIView) -> T? { let gestureRecognizer = self.nextGestureRecognizer _nextGestureRecognizer = nil switch config { case .registerNewRecognizerToTargetView: - reactiveView.view.addGestureRecognizer(gestureRecognizer) + reactiveView.view.addGestureRecognizer(gestureRecognizer!) default: () } - gestureRecognizer.view?.isUserInteractionEnabled = true - gestureRecognizer.isEnabled = enabled.value + gestureRecognizer?.view?.isUserInteractionEnabled = true + gestureRecognizer?.isEnabled = enabled.value return gestureRecognizer } From dab2e7de73cb8b4485b548b3ca192b9b9484db88 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 21 Apr 2017 16:59:32 -0400 Subject: [PATCH 08/60] Operators that use _map no longer transform velocity by default. Summary: Velocity transformation is now opt in. Only x() and y() opt in to this behavior. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3078 --- src/operators/foundation/_map.swift | 4 ++-- src/operators/x.swift | 2 +- src/operators/y.swift | 2 +- tests/unit/operator/foundation/_mapTests.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/operators/foundation/_map.swift b/src/operators/foundation/_map.swift index edfd70a..5ddaed2 100644 --- a/src/operators/foundation/_map.swift +++ b/src/operators/foundation/_map.swift @@ -24,7 +24,7 @@ extension MotionObservableConvertible { This operator is meant to be used when building other operators. */ - public func _map(_ name: String? = nil, args: [Any]? = nil, transform: @escaping (T) -> U) -> MotionObservable { + public func _map(_ name: String? = nil, args: [Any]? = nil, transformVelocity: Bool = false, transform: @escaping (T) -> U) -> MotionObservable { return _nextOperator(name, args: args, operation: { value, next in next(transform(value)) @@ -33,7 +33,7 @@ extension MotionObservableConvertible { switch event { case .add(let info): if let initialVelocity = info.initialVelocity { - transformedInitialVelocity = transform(initialVelocity as! T) + transformedInitialVelocity = transformVelocity ? transform(initialVelocity as! T) : initialVelocity } else { transformedInitialVelocity = nil } diff --git a/src/operators/x.swift b/src/operators/x.swift index d311942..4dc9561 100644 --- a/src/operators/x.swift +++ b/src/operators/x.swift @@ -23,7 +23,7 @@ extension MotionObservableConvertible where T == CGPoint { Extract the x value from a CGPoint. */ public func x() -> MotionObservable { - return _map(#function) { + return _map(#function, transformVelocity: true) { $0.x } } diff --git a/src/operators/y.swift b/src/operators/y.swift index ea21183..6c1d205 100644 --- a/src/operators/y.swift +++ b/src/operators/y.swift @@ -23,7 +23,7 @@ extension MotionObservableConvertible where T == CGPoint { Extract the y value from a CGPoint. */ public func y() -> MotionObservable { - return _map(#function) { + return _map(#function, transformVelocity: true) { $0.y } } diff --git a/tests/unit/operator/foundation/_mapTests.swift b/tests/unit/operator/foundation/_mapTests.swift index d3d9a2f..e97b4bc 100644 --- a/tests/unit/operator/foundation/_mapTests.swift +++ b/tests/unit/operator/foundation/_mapTests.swift @@ -121,7 +121,7 @@ class _mapTests: XCTestCase { } let eventReceived = expectation(description: "Event was received") - let _ = observable._map { value in + let _ = observable._map(transformVelocity: true) { value in return value * scalar }.subscribe(next: { _ in }, coreAnimation: { event in From 9b2347005135d11a7c28a5b27af8730c6467724b Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 24 Apr 2017 14:53:59 -0400 Subject: [PATCH 09/60] Ensure that system animations take effect during view controller transitions. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3092 --- src/transitions/TransitionContext.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index 8c22df5..7404b96 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -206,12 +206,28 @@ extension TransitionContext { self.runtime = MotionRuntime(containerView: containerView()) self.replicator.containerView = containerView() + pokeSystemAnimations() + let terminators = transition.willBeginTransition(withContext: self, runtime: self.runtime) runtime.whenAllAtRest(terminators) { [weak self] in self?.terminate() } } + // UIKit transitions will not animate any of the system animations (status bar changes, notably) + // unless we have at least one implicit UIView animation. Material Motion doesn't use implicit + // animations out of the box, so to ensure that system animations still occur we create an + // invisible throwaway view and apply an animation to it. + private func pokeSystemAnimations() { + let throwawayView = UIView() + containerView().addSubview(throwawayView) + UIView.animate(withDuration: transitionDuration(using: context), animations: { + throwawayView.frame = throwawayView.frame.offsetBy(dx: 1, dy: 0) + }, completion: { didComplete in + throwawayView.removeFromSuperview() + }) + } + private func terminate() { guard runtime != nil else { return } let completedInOriginalDirection = direction.value == initialDirection From a921f721ffc5c8b9cc75dc27b589eb949f5c7112 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 24 Apr 2017 15:25:05 -0400 Subject: [PATCH 10/60] View controllers are only interactive if at least one gesture recognizer is active. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3093 --- src/transitions/TransitionController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index 13bcc39..d583b58 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -238,7 +238,7 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni } func isInteractive() -> Bool { - return dismisser.gestureRecognizers.count > 0 + return gestureDelegate.gestureRecognizers.filter { $0.state == .began || $0.state == .changed }.count > 0 } } From 9fa2b22dabc9a7fcd7bfb9a702007db1680d8fd4 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 24 Apr 2017 15:52:47 -0400 Subject: [PATCH 11/60] Add a Manipulation type and implement UIKit view controller transitioning interactivity APIs. Summary: Draggable, Rotatable, and Scalable are now Manipulation types. When added to the runtime they will now affect the runtime's isBeingManipulated stream. We use this stream to inform UIKit when a transition becomes or stops being interactive. This ensures that system animations (e.g. status bar) start when the user stops interacting with the transition. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Subscribers: markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3094 --- examples/ContextualTransitionExample.swift | 4 ++ src/Interaction.swift | 5 +++ src/MotionRuntime.swift | 13 ++++++ src/interactions/Draggable.swift | 2 +- src/interactions/Rotatable.swift | 2 +- src/interactions/Scalable.swift | 2 +- src/transitions/TransitionContext.swift | 47 +++++++++++++++++++--- 7 files changed, 66 insertions(+), 9 deletions(-) diff --git a/examples/ContextualTransitionExample.swift b/examples/ContextualTransitionExample.swift index eb26aa6..2dac4e2 100644 --- a/examples/ContextualTransitionExample.swift +++ b/examples/ContextualTransitionExample.swift @@ -226,6 +226,10 @@ class PhotoAlbumViewController: UIViewController, UICollectionViewDataSource, UI collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) } + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return album.photos.count } diff --git a/src/Interaction.swift b/src/Interaction.swift index 2c2b52a..b65bcee 100644 --- a/src/Interaction.swift +++ b/src/Interaction.swift @@ -41,6 +41,11 @@ public protocol Interaction { func add(to target: Target, withRuntime runtime: MotionRuntime, constraints: Constraints?) } +/** + A manipulation is an object whose state represents direct manipulation from the user. + */ +public protocol Manipulation: Stateful {} + /** A typical constraint shape for an interaction. */ diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift index a299b98..a356e9b 100644 --- a/src/MotionRuntime.swift +++ b/src/MotionRuntime.swift @@ -68,6 +68,10 @@ public final class MotionRuntime { interactions.append(interaction) interaction.add(to: target, withRuntime: self, constraints: constraints) + if let manipulation = interaction as? Manipulation { + aggregateManipulationState.observe(state: manipulation.state, withRuntime: self) + } + let identifier = ObjectIdentifier(target) var targetInteractions = targets[identifier] ?? [] targetInteractions.append(interaction) @@ -235,6 +239,15 @@ public final class MotionRuntime { return lines.joined(separator: "\n") } + /** + A Boolean stream indicating whether the runtime is currently being directly manipulated by the + user. + */ + public var isBeingManipulated: MotionObservable { + return aggregateManipulationState.asStream().rewrite([.active: true, .atRest: false]) + } + private let aggregateManipulationState = AggregateMotionState() + private func write(_ stream: O, to property: ReactiveProperty) where O.T == T { metadata.append(stream.metadata.createChild(property.metadata)) subscriptions.append(stream.subscribe(next: { property.value = $0 }, diff --git a/src/interactions/Draggable.swift b/src/interactions/Draggable.swift index 503a121..e56896f 100644 --- a/src/interactions/Draggable.swift +++ b/src/interactions/Draggable.swift @@ -33,7 +33,7 @@ import UIKit - `{ $0.xLocked(to: somePosition) }` - `{ $0.yLocked(to: somePosition) }` */ -public final class Draggable: Gesturable, Interaction, Togglable, Stateful { +public final class Draggable: Gesturable, Interaction, Togglable, Manipulation { /** A sub-interaction for writing the next gesture recognizer's final velocity to a property. diff --git a/src/interactions/Rotatable.swift b/src/interactions/Rotatable.swift index d40257b..ea7388d 100644 --- a/src/interactions/Rotatable.swift +++ b/src/interactions/Rotatable.swift @@ -30,7 +30,7 @@ import UIKit CGFloat constraints may be applied to this interaction. */ -public final class Rotatable: Gesturable, Interaction, Togglable, Stateful { +public final class Rotatable: Gesturable, Interaction, Togglable, Manipulation { public func add(to view: UIView, withRuntime runtime: MotionRuntime, constraints applyConstraints: ConstraintApplicator? = nil) { diff --git a/src/interactions/Scalable.swift b/src/interactions/Scalable.swift index 5e4235a..3b8e894 100644 --- a/src/interactions/Scalable.swift +++ b/src/interactions/Scalable.swift @@ -30,7 +30,7 @@ import UIKit CGFloat constraints may be applied to this interaction. */ -public final class Scalable: Gesturable, Interaction, Togglable, Stateful { +public final class Scalable: Gesturable, Interaction, Togglable, Manipulation { public func add(to view: UIView, withRuntime runtime: MotionRuntime, constraints applyConstraints: ConstraintApplicator? = nil) { diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index 7404b96..afd1d22 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -119,6 +119,8 @@ public final class TransitionContext: NSObject { fileprivate var transition: Transition! fileprivate var context: UIViewControllerContextTransitioning! fileprivate var didRegisterTerminator = false + fileprivate var interactiveSubscription: Subscription? + fileprivate var isBeingManipulated = false } extension TransitionContext: UIViewControllerAnimatedTransitioning { @@ -212,6 +214,8 @@ extension TransitionContext { runtime.whenAllAtRest(terminators) { [weak self] in self?.terminate() } + + observeInteractiveState() } // UIKit transitions will not animate any of the system animations (status bar changes, notably) @@ -228,6 +232,35 @@ extension TransitionContext { }) } + // UIKit view controller transitions are either animated or interactive and we must inform UIKit + // when this state changes. Certain system animations (status bar) will not be initiated until + // interactivity has completed. We consider an "interactive transition" to be one that has one or + // more active Manipulation types. + private func observeInteractiveState() { + interactiveSubscription = runtime.isBeingManipulated.dedupe().subscribeToValue { [weak self] isBeingManipulated in + guard let strongSelf = self else { + return + } + strongSelf.isBeingManipulated = isBeingManipulated + + // Becoming interactive + if !strongSelf.context.isInteractive && isBeingManipulated { + if #available(iOS 10.0, *) { + strongSelf.context.pauseInteractiveTransition() + } + + // Becoming non-interactive + } else if strongSelf.context.isInteractive && !isBeingManipulated { + let completedInOriginalDirection = strongSelf.direction.value == strongSelf.initialDirection + if completedInOriginalDirection { + strongSelf.context.finishInteractiveTransition() + } else { + strongSelf.context.cancelInteractiveTransition() + } + } + } + } + private func terminate() { guard runtime != nil else { return } let completedInOriginalDirection = direction.value == initialDirection @@ -235,12 +268,14 @@ extension TransitionContext { // UIKit container view controllers will replay their transition animation if the transition // percentage is exactly 0 or 1, so we fake being super close to these values in order to avoid // this flickering animation. - if completedInOriginalDirection { - context.updateInteractiveTransition(0.999) - context.finishInteractiveTransition() - } else { - context.updateInteractiveTransition(0.001) - context.cancelInteractiveTransition() + if context.isInteractive { + if completedInOriginalDirection { + context.updateInteractiveTransition(0.999) + context.finishInteractiveTransition() + } else { + context.updateInteractiveTransition(0.001) + context.cancelInteractiveTransition() + } } context.completeTransition(completedInOriginalDirection) From eaecd862aabb193de5a45024f52365fa1fbcb027 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 24 Apr 2017 16:04:18 -0400 Subject: [PATCH 12/60] Make both push back case studies use a light status bar in the modal view. Summary: This helps test that UIKit animations are happening alongside material motion transitions. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3095 --- examples/InteractivePushBackTransitionExample.swift | 4 ++++ examples/PushBackTransitionExample.swift | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/examples/InteractivePushBackTransitionExample.swift b/examples/InteractivePushBackTransitionExample.swift index d21c66c..bb9e718 100644 --- a/examples/InteractivePushBackTransitionExample.swift +++ b/examples/InteractivePushBackTransitionExample.swift @@ -64,6 +64,10 @@ private class ModalViewController: UIViewController, UIGestureRecognizerDelegate scrollView.panGestureRecognizer.require(toFail: pan) view.addGestureRecognizer(pan) } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } } private class PushBackTransition: Transition { diff --git a/examples/PushBackTransitionExample.swift b/examples/PushBackTransitionExample.swift index 27cae79..60da235 100644 --- a/examples/PushBackTransitionExample.swift +++ b/examples/PushBackTransitionExample.swift @@ -59,6 +59,10 @@ private class ModalViewController: UIViewController { func didTap() { dismiss(animated: true) } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } } private class PushBackTransition: Transition { From dbff13d9018514e67d54742cac1dd4e176ac2b30 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 24 Apr 2017 16:34:22 -0400 Subject: [PATCH 13/60] Add a resistance property to Draggable. Summary: This new property makes it possible to configure resistance behaviors on Draggable when the user moves beyond a given perimeter. This API is preferred over applying a rubberBand constraint to a Tossable interaction because it ensures that resistance is only applied to the draggable output, not the spring (which may re-read the position value and then incorrectly cause the position to jump to the doubly-resisted position). Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3097 --- src/interactions/Draggable.swift | 21 +++++++++++++ src/operators/rubberBanded.swift | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/interactions/Draggable.swift b/src/interactions/Draggable.swift index e56896f..ff882a7 100644 --- a/src/interactions/Draggable.swift +++ b/src/interactions/Draggable.swift @@ -34,6 +34,25 @@ import UIKit - `{ $0.yLocked(to: somePosition) }` */ public final class Draggable: Gesturable, Interaction, Togglable, Manipulation { + + /** + When a non-null resistance perimiter is provided, dragging beyond the perimeter will result in + resistance being applied to the position until the max length is reached. + */ + public let resistance = ( + /** + The region beyond which resistance should take effect, in absolute coordinates. + + If .null, no resistance will be applied to the drag position. + */ + perimeter: createProperty(withInitialValue: CGRect.null), + + /** + The maximum distance the drag position is able to move beyond the perimeter. + */ + maxLength: createProperty(withInitialValue: 48) + ) + /** A sub-interaction for writing the next gesture recognizer's final velocity to a property. @@ -63,6 +82,8 @@ public final class Draggable: Gesturable, Interaction, T if let applyConstraints = applyConstraints { stream = applyConstraints(stream) } + stream = stream.rubberBanded(outsideOf: resistance.perimeter, + maxLength: resistance.maxLength) runtime.connect(stream, to: position) } } diff --git a/src/operators/rubberBanded.swift b/src/operators/rubberBanded.swift index ca9b65f..839ac5b 100644 --- a/src/operators/rubberBanded.swift +++ b/src/operators/rubberBanded.swift @@ -33,13 +33,65 @@ extension MotionObservableConvertible where T == CGPoint { /** Applies resistance to values that fall outside of the given range. + + Does not modify the value if CGRect is .null. */ public func rubberBanded(outsideOf rect: CGRect, maxLength: CGFloat) -> MotionObservable { return _map(#function, args: [rect, maxLength]) { + guard rect != .null else { + return $0 + } + return CGPoint(x: rubberBand(value: $0.x, min: rect.minX, max: rect.maxX, bandLength: maxLength), y: rubberBand(value: $0.y, min: rect.minY, max: rect.maxY, bandLength: maxLength)) } } + + /** + Applies resistance to values that fall outside of the given range. + + Does not modify the value if CGRect is .null. + */ + public func rubberBanded(outsideOf rectStream: O1, maxLength maxLengthStream: O2) -> MotionObservable where O1: MotionObservableConvertible, O1.T == CGRect, O2: MotionObservableConvertible, O2.T == CGFloat { + var lastRect: CGRect? + var lastMaxLength: CGFloat? + var lastValue: CGPoint? + return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [rectStream, maxLengthStream]))) { observer in + + let checkAndEmit = { + guard let rect = lastRect, let maxLength = lastMaxLength, let value = lastValue else { + return + } + guard lastRect != .null else { + observer.next(value) + return + } + observer.next(CGPoint(x: rubberBand(value: value.x, min: rect.minX, max: rect.maxX, bandLength: maxLength), + y: rubberBand(value: value.y, min: rect.minY, max: rect.maxY, bandLength: maxLength))) + } + + let rectSubscription = rectStream.subscribeToValue { rect in + lastRect = rect + checkAndEmit() + } + + let maxLengthSubscription = maxLengthStream.subscribeToValue { maxLength in + lastMaxLength = maxLength + checkAndEmit() + } + + let upstreamSubscription = self.subscribeAndForward(to: observer) { value in + lastValue = value + checkAndEmit() + } + + return { + rectSubscription.unsubscribe() + maxLengthSubscription.unsubscribe() + upstreamSubscription.unsubscribe() + } + } + } } private func rubberBand(value: CGFloat, min: CGFloat, max: CGFloat, bandLength: CGFloat) -> CGFloat { From d6ab9d281ea15f388fe0d099686bb812df3caba8 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 25 Apr 2017 14:00:56 -0400 Subject: [PATCH 14/60] Change runtime.interactions API to use an ofType: argument instead of a block. Summary: This simplifies the API and reduces the syntactical overhead of using it. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Subscribers: markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3099 --- src/MotionRuntime.swift | 12 ++++++++++-- tests/unit/MotionRuntimeTests.swift | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift index a356e9b..653fc37 100644 --- a/src/MotionRuntime.swift +++ b/src/MotionRuntime.swift @@ -83,13 +83,21 @@ public final class MotionRuntime { Example usage: - let draggables = runtime.interactions(for: view) { $0 as? Draggable } + let draggables = runtime.interactions(for: view, type: Draggable.self) */ + public func interactions(for target: I.Target, ofType: I.Type) -> [I] where I: Interaction, I.Target: AnyObject { + guard let interactions = targets[ObjectIdentifier(target)] else { + return [] + } + return interactions.flatMap { $0 as? I } + } + + @available(*, deprecated, message: "Use interactions(for:ofType:) instead.") public func interactions(for target: I.Target, filter: (Any) -> I?) -> [I] where I: Interaction, I.Target: AnyObject { guard let interactions = targets[ObjectIdentifier(target)] else { return [] } - return interactions.flatMap(filter) + return interactions.flatMap { $0 as? I } } /** diff --git a/tests/unit/MotionRuntimeTests.swift b/tests/unit/MotionRuntimeTests.swift index f65a123..91af52f 100644 --- a/tests/unit/MotionRuntimeTests.swift +++ b/tests/unit/MotionRuntimeTests.swift @@ -34,7 +34,7 @@ class MotionRuntimeTests: XCTestCase { func testInteractionsReturnsEmptyArrayWithoutAnyAddedInteractions() { let runtime = MotionRuntime(containerView: UIView()) - let results = runtime.interactions(for: UIView()) { $0 as? Draggable } + let results = runtime.interactions(for: UIView(), ofType: Draggable.self) XCTAssertEqual(results.count, 0) } @@ -45,7 +45,7 @@ class MotionRuntimeTests: XCTestCase { runtime.add(Draggable(), to: view) runtime.add(Rotatable(), to: view) - let results = runtime.interactions(for: view) { $0 as? Draggable } + let results = runtime.interactions(for: view, ofType: Draggable.self) XCTAssertEqual(results.count, 1) } } From 5022aad2777f16fcb373e2c155e1f0d47ada9a36 Mon Sep 17 00:00:00 2001 From: Eric Tang Date: Tue, 25 Apr 2017 16:46:52 -0400 Subject: [PATCH 15/60] Add repeat APIs to Tween Summary: Added API to Tween to allow animations to repeat Fixes https://github.com/material-motion/material-motion-swift/issues/116 Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, featherless Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, featherless Subscribers: featherless Tags: #material_motion Differential Revision: http://codereview.cc/D3111 --- src/interactions/Tween.swift | 33 ++++++++++++++++++++ src/systems/coreAnimationTweenToStream.swift | 3 ++ 2 files changed, 36 insertions(+) diff --git a/src/interactions/Tween.swift b/src/interactions/Tween.swift index 0524f21..7c843c8 100644 --- a/src/interactions/Tween.swift +++ b/src/interactions/Tween.swift @@ -32,6 +32,33 @@ public class Tween: Interaction, Togglable, Stateful { */ public let duration: ReactiveProperty + /** + The number of times the animation will repeat. + + If the repeatCount is 0, it is ignored. If both repeatDuration and repeatCount are specified the behavior is undefined. + + Setting this property to greatestFiniteMagnitude will cause the animation to repeat forever. + + See https://developer.apple.com/reference/quartzcore/camediatiming/1427666-repeatcount for more information. + */ + public let repeatCount: ReactiveProperty = createProperty("Tween.repeatCount", withInitialValue: 0) + + /** + The number of seconds the animation will repeat for. + + If the repeatDuration is 0, it is ignored. If both repeatDuration and repeatCount are specified the behavior is undefined. + + See https://developer.apple.com/reference/quartzcore/camediatiming/1427643-repeatduration for more information. + */ + public let repeatDuration: ReactiveProperty = createProperty("Tween.repeatDuration", withInitialValue: 0) + + /** + Will the animation play in the reverse upon completion. + + See https://developer.apple.com/reference/quartzcore/camediatiming/1427645-autoreverses for more information. + */ + public let autoreverses: ReactiveProperty = createProperty("Tween.autoreverses", withInitialValue: false) + /** The delay of the animation in seconds. */ @@ -137,6 +164,9 @@ public struct TweenShadow { public let state: ReactiveProperty public let duration: ReactiveProperty public let delay: ReactiveProperty + public let repeatCount: ReactiveProperty + public let repeatDuration: ReactiveProperty + public let autoreverses: ReactiveProperty public let values: ReactiveProperty<[T]> public let keyPositions: ReactiveProperty<[CGFloat]> public let timingFunctions: ReactiveProperty<[CAMediaTimingFunction]> @@ -146,6 +176,9 @@ public struct TweenShadow { self.enabled = tween.enabled self.state = tween._state self.duration = tween.duration + self.repeatCount = tween.repeatCount + self.repeatDuration = tween.repeatDuration + self.autoreverses = tween.autoreverses self.delay = tween.delay self.values = tween.values self.keyPositions = tween.keyPositions diff --git a/src/systems/coreAnimationTweenToStream.swift b/src/systems/coreAnimationTweenToStream.swift index e455972..ce52240 100644 --- a/src/systems/coreAnimationTweenToStream.swift +++ b/src/systems/coreAnimationTweenToStream.swift @@ -67,6 +67,9 @@ private func streamFromTween(_ tween: TweenShadow, configureEvent: @escapi } animation.beginTime = CFTimeInterval(tween.delay.value) animation.duration = CFTimeInterval(duration) + animation.repeatCount = Float(tween.repeatCount.value) + animation.repeatDuration = CFTimeInterval(tween.repeatDuration.value) + animation.autoreverses = tween.autoreverses.value let key = NSUUID().uuidString activeAnimations.insert(key) From 7adfe179843218713e11001a7839144b0203124f Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 27 Apr 2017 12:53:57 -0400 Subject: [PATCH 16/60] Bump IndefiniteObservable to 4.0 and add explicit unsubscriptions to the runtime. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, appsforartists Reviewed By: O2 Material Motion, #material_motion, appsforartists Tags: #material_motion Differential Revision: http://codereview.cc/D3124 --- MaterialMotion.podspec | 2 +- Podfile.lock | 16 ++++++++-------- src/MotionRuntime.swift | 1 + tests/unit/operator/delayTests.swift | 3 ++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/MaterialMotion.podspec b/MaterialMotion.podspec index 97d38ba..bf4ca1c 100644 --- a/MaterialMotion.podspec +++ b/MaterialMotion.podspec @@ -11,5 +11,5 @@ Pod::Spec.new do |s| s.source_files = "src/**/*.{swift}" - s.dependency "IndefiniteObservable", "~> 3.0" + s.dependency "IndefiniteObservable", "~> 4.0" end diff --git a/Podfile.lock b/Podfile.lock index 3613014..58a2b25 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,10 +1,10 @@ PODS: - - CatalogByConvention (2.0.0) - - IndefiniteObservable (3.1.0): - - IndefiniteObservable/lib (= 3.1.0) - - IndefiniteObservable/lib (3.1.0) + - CatalogByConvention (2.1.0) + - IndefiniteObservable (4.0.0): + - IndefiniteObservable/lib (= 4.0.0) + - IndefiniteObservable/lib (4.0.0) - MaterialMotion (1.3.0): - - IndefiniteObservable (~> 3.0) + - IndefiniteObservable (~> 4.0) DEPENDENCIES: - CatalogByConvention @@ -15,9 +15,9 @@ EXTERNAL SOURCES: :path: "./" SPEC CHECKSUMS: - CatalogByConvention: be55c2263132e4f9f59299ac8a528ee8715b3275 - IndefiniteObservable: 2789d61f487d8d37fa2b9c3153cc44d4447ff744 - MaterialMotion: b5040104b109cf9680a2c4a77296c429dab2c376 + CatalogByConvention: ef0913973b86b4234bcadf22aa84037c4a47cbbd + IndefiniteObservable: 1ee6af37efa8763a222cc6d414cd125e26ed9b6a + MaterialMotion: 42e9f95334ad208e9d44536d188d75488d847c81 PODFILE CHECKSUM: f503265a0d60526a0d28c96dd4bdcfb40fb562fc diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift index 653fc37..6f1de71 100644 --- a/src/MotionRuntime.swift +++ b/src/MotionRuntime.swift @@ -34,6 +34,7 @@ public final class MotionRuntime { deinit { _visualizationView?.removeFromSuperview() + subscriptions.forEach { $0.unsubscribe() } } /** diff --git a/tests/unit/operator/delayTests.swift b/tests/unit/operator/delayTests.swift index 968308e..8b8f332 100644 --- a/tests/unit/operator/delayTests.swift +++ b/tests/unit/operator/delayTests.swift @@ -58,9 +58,10 @@ class delayTests: XCTestCase { func testValueIsNotReceivedWithoutSubscription() { let property = createProperty() - let _ = property.delay(by: 0.01).subscribeToValue { value in + let subscription = property.delay(by: 0.01).subscribeToValue { value in assertionFailure("Should not be received.") } + subscription.unsubscribe() let delay = expectation(description: "Delay") DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(Int(0.05 * 1000))) { From f83c027067fe0ebcc0792d69648362660bcb3279 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 26 Apr 2017 15:21:18 -0400 Subject: [PATCH 17/60] Move Timeline to a timeline folder. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, appsforartists Reviewed By: O2 Material Motion, #material_motion, appsforartists Tags: #material_motion Differential Revision: http://codereview.cc/D3115 --- src/{ => timeline}/Timeline.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{ => timeline}/Timeline.swift (100%) diff --git a/src/Timeline.swift b/src/timeline/Timeline.swift similarity index 100% rename from src/Timeline.swift rename to src/timeline/Timeline.swift From d584666c449e5a6304a22e8f2fcdcb1d66b6e56a Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 27 Apr 2017 13:27:48 -0400 Subject: [PATCH 18/60] Reduce flakiness of delay test. Summary: The test was delaying the stream execution by 500ms and waiting on the expectation for 500ms, meaning the test could fail in the event that the wait happened to end before the stream executed. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3114 --- tests/unit/operator/delayTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/operator/delayTests.swift b/tests/unit/operator/delayTests.swift index 8b8f332..ed721e9 100644 --- a/tests/unit/operator/delayTests.swift +++ b/tests/unit/operator/delayTests.swift @@ -42,7 +42,7 @@ class delayTests: XCTestCase { var hasReceived = false let didReceiveValue = expectation(description: "Did receive value") - let subscription = property.delay(by: .milliseconds(500)).subscribeToValue { value in + let subscription = property.delay(by: .milliseconds(300)).subscribeToValue { value in XCTAssertEqual(value, 0) didReceiveValue.fulfill() hasReceived = true From 9c5010e54b8380eae6ce337408aeb1778fc39cc8 Mon Sep 17 00:00:00 2001 From: Eric Tang Date: Thu, 27 Apr 2017 13:29:26 -0400 Subject: [PATCH 19/60] Added new start function to MotionRuntime Summary: Added a new start function to MotionRuntime that will allow interaction B to start when interaction A is at certain state (active or atRest) Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, featherless Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, featherless Subscribers: featherless Tags: #material_motion Differential Revision: http://codereview.cc/D3117 --- src/MotionRuntime.swift | 7 ++++++ tests/unit/MotionRuntimeTests.swift | 36 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift index 6f1de71..360b403 100644 --- a/src/MotionRuntime.swift +++ b/src/MotionRuntime.swift @@ -125,6 +125,13 @@ public final class MotionRuntime { connect(state.rewrite([.active: true]), to: interaction.enabled) } + /** + Initiates interaction B when interaction A changes to certain state + */ + public func start(_ interactionA: Togglable, when interactionB: Stateful, is state: MotionState) { + connect(interactionB.state.dedupe().rewrite([state: true]), to: interactionA.enabled) + } + /** Connects a stream's output to a reactive property. diff --git a/tests/unit/MotionRuntimeTests.swift b/tests/unit/MotionRuntimeTests.swift index 91af52f..58a2d8f 100644 --- a/tests/unit/MotionRuntimeTests.swift +++ b/tests/unit/MotionRuntimeTests.swift @@ -48,4 +48,40 @@ class MotionRuntimeTests: XCTestCase { let results = runtime.interactions(for: view, ofType: Draggable.self) XCTAssertEqual(results.count, 1) } + + public class MockTween: Togglable, Stateful { + let _state = createProperty("Tween._state", withInitialValue: MotionState.atRest) + public let enabled = createProperty("Tween.enabled", withInitialValue: true) + public func setState(state: MotionState) { + _state.value = state + } + public var state: MotionObservable { + return _state.asStream() + } + } + + func testRuntimeStart() { + let promise = expectation(description: "start interaction B when interaction A is at state") + + let view = UIView() + let runtime = MotionRuntime(containerView: view) + let tweenA = MockTween() + tweenA.setState(state: .active) + + let tweenB = MockTween() + tweenB.setState(state: .atRest) + tweenB.enabled.value = false + runtime.start(tweenB, when: tweenA, is: .atRest) + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) { + tweenA.setState(state: MotionState.atRest) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(15)) { + XCTAssertEqual(tweenB.enabled.value, true) + promise.fulfill() + } + + waitForExpectations(timeout: 100, handler: nil) + } } From 67cb7b18dccce117e1a7d248e0919b4f9edb613d Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 27 Apr 2017 13:30:08 -0400 Subject: [PATCH 20/60] Shorten the delayBy test delay. Summary: Speeding up the tests. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, appsforartists Reviewed By: O2 Material Motion, #material_motion, appsforartists Tags: #material_motion Differential Revision: http://codereview.cc/D3125 --- tests/unit/operator/delayTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/operator/delayTests.swift b/tests/unit/operator/delayTests.swift index ed721e9..4c85cbb 100644 --- a/tests/unit/operator/delayTests.swift +++ b/tests/unit/operator/delayTests.swift @@ -42,7 +42,7 @@ class delayTests: XCTestCase { var hasReceived = false let didReceiveValue = expectation(description: "Did receive value") - let subscription = property.delay(by: .milliseconds(300)).subscribeToValue { value in + let subscription = property.delay(by: .milliseconds(10)).subscribeToValue { value in XCTAssertEqual(value, 0) didReceiveValue.fulfill() hasReceived = true @@ -50,7 +50,7 @@ class delayTests: XCTestCase { XCTAssertFalse(hasReceived) - waitForExpectations(timeout: 0.5) + waitForExpectations(timeout: 0.1) subscription.unsubscribe() } From be0ea019c4d2db5ec63e625895f64ec73ca7b702 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 27 Apr 2017 15:21:15 -0400 Subject: [PATCH 21/60] Add a new Reactive type for querying reactive properties. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator Reviewed By: randcode-generator Tags: #material_motion Differential Revision: http://codereview.cc/D3112 --- examples/ContextualTransitionExample.swift | 2 +- examples/FabTransitionExample.swift | 4 +- src/MotionRuntime.swift | 18 +- src/interactions/Draggable.swift | 2 +- src/interactions/Rotatable.swift | 2 +- src/interactions/Scalable.swift | 2 +- src/protocols/Gesturable.swift | 4 +- src/reactivetypes/Reactive+CALayer.swift | 299 +++++++++++++++ src/reactivetypes/Reactive+CAShapeLayer.swift | 33 ++ src/reactivetypes/Reactive+UIView.swift | 58 +++ src/reactivetypes/Reactive.swift | 111 ++++++ src/reactivetypes/ReactiveCALayer.swift | 348 ------------------ src/reactivetypes/ReactiveUIView.swift | 53 --- src/timeline/CALayer+Timeline.swift | 68 ++++ tests/unit/MotionRuntimeTests.swift | 2 +- tests/unit/ReactivePropertyTests.swift | 68 ++++ 16 files changed, 655 insertions(+), 419 deletions(-) create mode 100644 src/reactivetypes/Reactive+CALayer.swift create mode 100644 src/reactivetypes/Reactive+CAShapeLayer.swift create mode 100644 src/reactivetypes/Reactive+UIView.swift create mode 100644 src/reactivetypes/Reactive.swift delete mode 100644 src/reactivetypes/ReactiveCALayer.swift delete mode 100644 src/reactivetypes/ReactiveUIView.swift create mode 100644 src/timeline/CALayer+Timeline.swift diff --git a/examples/ContextualTransitionExample.swift b/examples/ContextualTransitionExample.swift index 2dac4e2..db4ba3e 100644 --- a/examples/ContextualTransitionExample.swift +++ b/examples/ContextualTransitionExample.swift @@ -285,7 +285,7 @@ private class PushBackTransition: Transition { let size = TransitionSpring(back: contextView.bounds.size, fore: fitSize, direction: ctx.direction) runtime.toggle(size, inReactionTo: draggable) - runtime.add(size, to: runtime.get(replicaView).reactiveLayer.size) + runtime.add(size, to: runtime.get(replicaView).layer.size) let opacity = TransitionSpring(back: 0, fore: 1, direction: ctx.direction) runtime.add(opacity, to: runtime.get(ctx.fore.view.layer).opacity) diff --git a/examples/FabTransitionExample.swift b/examples/FabTransitionExample.swift index b74b443..fd6189c 100644 --- a/examples/FabTransitionExample.swift +++ b/examples/FabTransitionExample.swift @@ -124,14 +124,14 @@ private class CircularRevealTransition: Transition { let foreShadowPath = CGRect(origin: .zero(), size: expandedSize) let shadowPath = tween(back: floodFillView.layer.shadowPath!, fore: UIBezierPath(ovalIn: foreShadowPath).cgPath, ctx: ctx) - let floodLayer = runtime.get(floodFillView).reactiveLayer + let floodLayer = runtime.get(floodFillView).layer runtime.add(expansion, to: floodLayer.size) runtime.add(fadeOut, to: floodLayer.opacity) runtime.add(radius, to: floodLayer.cornerRadius) runtime.add(shadowPath, to: floodLayer.shadowPath) let shiftIn = tween(back: ctx.fore.view.layer.position.y + 40, fore: ctx.fore.view.layer.position.y, ctx: ctx) - runtime.add(shiftIn, to: runtime.get(ctx.fore.view).reactiveLayer.positionY) + runtime.add(shiftIn, to: runtime.get(ctx.fore.view).layer.positionY) let maskShiftIn = tween(back: CGFloat(-40), fore: CGFloat(0), ctx: ctx) runtime.add(maskShiftIn, to: runtime.get(maskLayer).positionY) diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift index 360b403..83850c8 100644 --- a/src/MotionRuntime.swift +++ b/src/MotionRuntime.swift @@ -170,24 +170,24 @@ public final class MotionRuntime { // MARK: Reactive object storage /** - Returns a reactive version of the given object and caches the returned result for future access. + Returns a reactive version of the given object. */ - public func get(_ view: UIView) -> ReactiveUIView { - return get(view) { .init($0, runtime: self) } + public func get(_ view: UIView) -> Reactive { + return Reactive(view) } /** - Returns a reactive version of the given object and caches the returned result for future access. + Returns a reactive version of the given object. */ - public func get(_ layer: CALayer) -> ReactiveCALayer { - return get(layer) { .init($0) } + public func get(_ layer: CALayer) -> Reactive { + return Reactive(layer) } /** - Returns a reactive version of the given object and caches the returned result for future access. + Returns a reactive version of the given object. */ - public func get(_ layer: CAShapeLayer) -> ReactiveCAShapeLayer { - return get(layer) { .init($0) } + public func get(_ layer: CAShapeLayer) -> Reactive { + return Reactive(layer) } /** diff --git a/src/interactions/Draggable.swift b/src/interactions/Draggable.swift index ff882a7..8bb0c1f 100644 --- a/src/interactions/Draggable.swift +++ b/src/interactions/Draggable.swift @@ -69,7 +69,7 @@ public final class Draggable: Gesturable, Interaction, T guard let gestureRecognizer = dequeueGestureRecognizer(withReactiveView: reactiveView) else { return } - let position = reactiveView.reactiveLayer.position + let position = reactiveView.layer.position runtime.connect(enabled, to: ReactiveProperty(initialValue: gestureRecognizer.isEnabled) { enabled in gestureRecognizer.isEnabled = enabled diff --git a/src/interactions/Rotatable.swift b/src/interactions/Rotatable.swift index ea7388d..3a27f90 100644 --- a/src/interactions/Rotatable.swift +++ b/src/interactions/Rotatable.swift @@ -38,7 +38,7 @@ public final class Rotatable: Gesturable, Interacti guard let gestureRecognizer = dequeueGestureRecognizer(withReactiveView: reactiveView) else { return } - let rotation = reactiveView.reactiveLayer.rotation + let rotation = reactiveView.layer.rotation runtime.connect(enabled, to: ReactiveProperty(initialValue: gestureRecognizer.isEnabled) { enabled in gestureRecognizer.isEnabled = enabled diff --git a/src/interactions/Scalable.swift b/src/interactions/Scalable.swift index 3b8e894..ac445db 100644 --- a/src/interactions/Scalable.swift +++ b/src/interactions/Scalable.swift @@ -38,7 +38,7 @@ public final class Scalable: Gesturable, Interaction, guard let gestureRecognizer = dequeueGestureRecognizer(withReactiveView: reactiveView) else { return } - let scale = reactiveView.reactiveLayer.scale + let scale = reactiveView.layer.scale runtime.connect(enabled, to: ReactiveProperty(initialValue: gestureRecognizer.isEnabled) { enabled in gestureRecognizer.isEnabled = enabled diff --git a/src/protocols/Gesturable.swift b/src/protocols/Gesturable.swift index a833712..2527f1f 100644 --- a/src/protocols/Gesturable.swift +++ b/src/protocols/Gesturable.swift @@ -124,13 +124,13 @@ public class Gesturable { /** Prepares and returns the gesture recognizer that should be used to drive this interaction. */ - func dequeueGestureRecognizer(withReactiveView reactiveView: ReactiveUIView) -> T? { + func dequeueGestureRecognizer(withReactiveView reactiveView: Reactive) -> T? { let gestureRecognizer = self.nextGestureRecognizer _nextGestureRecognizer = nil switch config { case .registerNewRecognizerToTargetView: - reactiveView.view.addGestureRecognizer(gestureRecognizer!) + reactiveView._object.addGestureRecognizer(gestureRecognizer!) default: () } diff --git a/src/reactivetypes/Reactive+CALayer.swift b/src/reactivetypes/Reactive+CALayer.swift new file mode 100644 index 0000000..ce5ac4b --- /dev/null +++ b/src/reactivetypes/Reactive+CALayer.swift @@ -0,0 +1,299 @@ +/* + Copyright 2016-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +extension Reactive where O: CALayer { + + public var anchorPoint: ReactiveProperty { + let layer = _object + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: layer.anchorPoint, + externalWrite: { layer.anchorPoint = $0 }, + keyPath: "anchorPoint") + } + } + + public var anchorPointAdjustment: ReactiveProperty { + let anchorPoint = self.anchorPoint + let position = self.position + let layer = _object + return _properties.named(#function) { + return .init("\(pretty(layer)).\(#function)", initialValue: .init(anchorPoint: anchorPoint.value, position: position.value)) { + anchorPoint.value = $0.anchorPoint; position.value = $0.position + } + } + } + + public var cornerRadius: ReactiveProperty { + let layer = _object + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: layer.cornerRadius, + externalWrite: { layer.cornerRadius = $0 }, + keyPath: "cornerRadius") + } + } + + public var height: ReactiveProperty { + let size = self.size + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: size.value.height, + externalWrite: { var dimensions = size.value; dimensions.height = $0; size.value = dimensions }, + keyPath: "bounds.size.height") + } + } + + public var opacity: ReactiveProperty { + let layer = _object + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: CGFloat(layer.opacity), + externalWrite: { layer.opacity = Float($0) }, + keyPath: "opacity") + } + } + + public var position: ReactiveProperty { + let layer = _object + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: layer.position, + externalWrite: { layer.position = $0 }, + keyPath: "position") + } + } + + public var positionX: ReactiveProperty { + let position = self.position + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: position.value.x, + externalWrite: { var point = position.value; point.x = $0; position.value = point }, + keyPath: "position.x") + } + } + + public var positionY: ReactiveProperty { + let position = self.position + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: position.value.y, + externalWrite: { var point = position.value; point.y = $0; position.value = point }, + keyPath: "position.y") + } + } + + public var rotation: ReactiveProperty { + let layer = _object + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: layer.value(forKeyPath: "transform.rotation.z") as! CGFloat, + externalWrite: { layer.setValue($0, forKeyPath: "transform.rotation.z") }, + keyPath: "transform.rotation.z") + } + } + + public var scale: ReactiveProperty { + let layer = _object + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: layer.value(forKeyPath: "transform.scale") as! CGFloat, + externalWrite: { layer.setValue($0, forKeyPath: "transform.scale") }, + keyPath: "transform.scale.xy") + } + } + + public var size: ReactiveProperty { + let layer = _object + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: layer.bounds.size, + externalWrite: { layer.bounds.size = $0 }, + keyPath: "bounds.size") + } + } + + public var shadowPath: ReactiveProperty { + let layer = _object + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: layer.shadowPath!, + externalWrite: { layer.shadowPath = $0 }, + keyPath: "shadowPath") + } + } + + public var width: ReactiveProperty { + let size = self.size + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: size.value.width, + externalWrite: { var dimensions = size.value; dimensions.width = $0; size.value = dimensions }, + keyPath: "bounds.size.width") + } + } + + func createCoreAnimationProperty(_ name: String, initialValue: T, externalWrite: @escaping NextChannel, keyPath: String) -> ReactiveProperty { + let layer = _object + var decomposedKeys = Set() + let property = ReactiveProperty("\(pretty(layer)).\(name)", initialValue: initialValue, externalWrite: { value in + let actionsWereDisabled = CATransaction.disableActions() + CATransaction.setDisableActions(true) + externalWrite(value) + CATransaction.setDisableActions(actionsWereDisabled) + }, coreAnimation: { event in + switch event { + case .add(let info): + if let timeline = info.timeline { + layer.timeline = timeline + } + + let animation = info.animation.copy() as! CAPropertyAnimation + + animation.duration *= TimeInterval(simulatorDragCoefficient()) + + if layer.speed == 0, let lastTimelineState = layer.lastTimelineState { + animation.beginTime = TimeInterval(lastTimelineState.beginTime) + animation.beginTime + } else { + animation.beginTime = layer.convertTime(CACurrentMediaTime(), from: nil) + animation.beginTime + } + + animation.keyPath = keyPath + + if let unsafeMakeAdditive = info.makeAdditive { + let makeAdditive: ((Any, Any) -> Any) = { from, to in + // When mapping properties to properties it's possible for the values to get implicitly + // wrapped in an NSNumber instance. This may cause the generic makeAdditive + // implementation to fail to cast to T, so we unbox the type here instead. + if let from = from as? NSNumber, let to = to as? NSNumber { + return from.doubleValue - to.doubleValue + } + return unsafeMakeAdditive(from, to) + } + + if let basicAnimation = animation as? CABasicAnimation { + basicAnimation.fromValue = makeAdditive(basicAnimation.fromValue!, basicAnimation.toValue!) + basicAnimation.toValue = makeAdditive(basicAnimation.toValue!, basicAnimation.toValue!) + basicAnimation.isAdditive = true + + } else if let keyframeAnimation = animation as? CAKeyframeAnimation { + let lastValue = keyframeAnimation.values!.last! + keyframeAnimation.values = keyframeAnimation.values!.map { makeAdditive($0, lastValue) } + keyframeAnimation.isAdditive = true + } + } + + // Core Animation springs do not support multi-dimensional velocity, so we bear the burden + // of decomposing multi-dimensional springs here. + if let springAnimation = animation as? CASpringAnimation + , springAnimation.isAdditive + , let initialVelocity = info.initialVelocity as? CGPoint + , let delta = springAnimation.fromValue as? CGPoint { + let decomposed = decompose(springAnimation: springAnimation, + delta: delta, + initialVelocity: initialVelocity) + + CATransaction.begin() + CATransaction.setCompletionBlock(info.onCompletion) + layer.add(decomposed.0, forKey: info.key + ".x") + layer.add(decomposed.1, forKey: info.key + ".y") + CATransaction.commit() + + decomposedKeys.insert(info.key) + return + } + + if let initialVelocity = info.initialVelocity { + applyInitialVelocity(initialVelocity, to: animation) + } + + CATransaction.begin() + CATransaction.setCompletionBlock(info.onCompletion) + layer.add(animation, forKey: info.key + "." + keyPath) + CATransaction.commit() + + case .remove(let key): + if let presentationLayer = layer.presentation() { + layer.setValue(presentationLayer.value(forKeyPath: keyPath), forKeyPath: keyPath) + } + if decomposedKeys.contains(key) { + layer.removeAnimation(forKey: key + ".x") + layer.removeAnimation(forKey: key + ".y") + decomposedKeys.remove(key) + + } else { + layer.removeAnimation(forKey: key + "." + keyPath) + } + } + }) + var lastView: UIView? + property.shouldVisualizeMotion = { view, containerView in + if lastView != view, let lastView = lastView { + lastView.removeFromSuperview() + } + view.isUserInteractionEnabled = false + if let superlayer = layer.superlayer { + view.frame = superlayer.convert(superlayer.bounds, to: containerView.layer) + } else { + view.frame = containerView.bounds + } + containerView.addSubview(view) + lastView = view + } + + return property + } +} + +private func decompose(springAnimation: CASpringAnimation, delta: CGPoint, initialVelocity: CGPoint) -> (CASpringAnimation, CASpringAnimation) { + let xAnimation = springAnimation.copy() as! CASpringAnimation + let yAnimation = springAnimation.copy() as! CASpringAnimation + xAnimation.fromValue = delta.x + yAnimation.fromValue = delta.y + xAnimation.toValue = 0 + yAnimation.toValue = 0 + + if delta.x != 0 { + xAnimation.initialVelocity = initialVelocity.x / -delta.x + } + if delta.y != 0 { + yAnimation.initialVelocity = initialVelocity.y / -delta.y + } + + xAnimation.keyPath = springAnimation.keyPath! + ".x" + yAnimation.keyPath = springAnimation.keyPath! + ".y" + + return (xAnimation, yAnimation) +} + +private func applyInitialVelocity(_ initialVelocity: Any, to animation: CAPropertyAnimation) { + if let springAnimation = animation as? CASpringAnimation, springAnimation.isAdditive { + // Additive animations have a toValue of 0 and a fromValue of negative delta (where the model + // value came from). + guard let initialVelocity = initialVelocity as? CGFloat, let delta = springAnimation.fromValue as? CGFloat else { + // Unsupported velocity type. + return + } + if delta != 0 { + // CASpringAnimation's initialVelocity is proportional to the distance to travel, i.e. our + // delta. + springAnimation.initialVelocity = initialVelocity / -delta + } + } +} diff --git a/src/reactivetypes/Reactive+CAShapeLayer.swift b/src/reactivetypes/Reactive+CAShapeLayer.swift new file mode 100644 index 0000000..c109b19 --- /dev/null +++ b/src/reactivetypes/Reactive+CAShapeLayer.swift @@ -0,0 +1,33 @@ +/* + Copyright 2016-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +import Foundation + +extension Reactive where O: CAShapeLayer { + + public var path: ReactiveProperty { + let layer = _object + return _properties.named(#function) { + return createCoreAnimationProperty(#function, + initialValue: layer.path!, + externalWrite: { layer.path = $0 }, + keyPath: "path") + } + } + +} diff --git a/src/reactivetypes/Reactive+UIView.swift b/src/reactivetypes/Reactive+UIView.swift new file mode 100644 index 0000000..76c8d75 --- /dev/null +++ b/src/reactivetypes/Reactive+UIView.swift @@ -0,0 +1,58 @@ +/* + Copyright 2016-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +extension Reactive where O: UIView { + + public var isUserInteractionEnabled: ReactiveProperty { + let view = _object + return _properties.named(#function) { + return .init("\(pretty(view)).\(#function)", initialValue: view.isUserInteractionEnabled) { + view.isUserInteractionEnabled = $0 + } + } + } + + public var backgroundColor: ReactiveProperty { + let view = _object + return _properties.named(#function) { + return .init("\(pretty(view)).\(#function)", initialValue: view.backgroundColor!) { + view.backgroundColor = $0 + } + } + } + + public var alpha: ReactiveProperty { + let view = _object + return _properties.named(#function) { + return .init("\(pretty(view)).\(#function)", initialValue: view.alpha) { + view.alpha = $0 + } + } + } + + public var layer: Reactive { + let view = _object + return Reactive(view.layer) + } + + @available(*, deprecated, message: "Use layer instead.") + public var reactiveLayer: Reactive { + let view = _object + return Reactive(view.layer) + } +} diff --git a/src/reactivetypes/Reactive.swift b/src/reactivetypes/Reactive.swift new file mode 100644 index 0000000..cd4ba92 --- /dev/null +++ b/src/reactivetypes/Reactive.swift @@ -0,0 +1,111 @@ +/* + Copyright 2016-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/** + A reactive representation of an object uses a global cache to fetch reactive property instances. + + Reactive property instances for a given object are shared across reactive instances. E.g. + + Reactive(view).position === Reactive(view).position // true + + ## Memory considerations + + - Reactive instances keep a strong reference to their object. + - Reactive properties are weakly held by the global property cache. + + ## Extending this type + + Use protocol extensions to extend this type for specific object types. For example: + + extension Reactive where O: UIView { + + public var isUserInteractionEnabled: ReactiveProperty { + let view = _object + return _properties.named(#function) { + return .init(initialValue: view.isUserInteractionEnabled) { + view.isUserInteractionEnabled = $0 + } + } + } + */ +public final class Reactive { + + /** + Creates a reactive representation of the given object. + */ + public init(_ object: O) { + self._object = object + + if let cache = globalCache.object(forKey: object) { + self._properties = cache + } else { + let cache = ReactiveCache() + globalCache.setObject(cache, forKey: object) + self._properties = cache + } + } + + /** + The object backing this reactive instance. + */ + public let _object: O + + /** + The property cache for this object instance. + */ + public let _properties: ReactiveCache +} + +/** + A reactive cache is created for an object as weak storage for reactive properties. + + Properties can be queried by name. The cache does not keep a strong reference to the stored + properties. If no references are kept to a queried property then it will be released and a new + reactive property will be returned on a subsequent invocation. + */ +public final class ReactiveCache: CustomDebugStringConvertible { + + /** + Queries a property with a given name, creating a new instance if no property exists yet. + + onCacheMiss is invoked if the property is not cached. The returned reactive property will be + stored in the cache and returned. + */ + func named(_ name: String, onCacheMiss: () -> ReactiveProperty) -> ReactiveProperty { + if let property = cache.object(forKey: name as NSString) { + return property as! ReactiveProperty + } + let property = onCacheMiss() + cache.setObject(property, forKey: name as NSString) + return property + } + + // Reactive properties are weakly held because they hold a reference to the object. If we kept a + // strong reference to the property then the globalCache weak key for the object would never reach + // zero and we'd have a memory leak, even if the property, object, and reactive instance were all + // dereferenced in client code. + private let cache = NSMapTable(keyOptions: .strongMemory, + valueOptions: [.weakMemory, .objectPointerPersonality]) + + public var debugDescription: String { + return cache.debugDescription + } +} + +private var globalCache = NSMapTable(keyOptions: [.weakMemory, .objectPointerPersonality], + valueOptions: [.strongMemory, .objectPointerPersonality]) diff --git a/src/reactivetypes/ReactiveCALayer.swift b/src/reactivetypes/ReactiveCALayer.swift deleted file mode 100644 index 674ab22..0000000 --- a/src/reactivetypes/ReactiveCALayer.swift +++ /dev/null @@ -1,348 +0,0 @@ -/* - Copyright 2016-present The Material Motion Authors. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import IndefiniteObservable -import UIKit - -public class ReactiveCALayer { - public let layer: CALayer - - public lazy var cornerRadius: ReactiveProperty = { - let layer = self.layer - return createCoreAnimationProperty(#function, - initialValue: layer.cornerRadius, - externalWrite: { layer.cornerRadius = $0 }, - keyPath: "cornerRadius", - reactiveLayer: self) - }() - - public lazy var opacity: ReactiveProperty = { - let layer = self.layer - return createCoreAnimationProperty(#function, - initialValue: CGFloat(layer.opacity), - externalWrite: { layer.opacity = Float($0) }, - keyPath: "opacity", - reactiveLayer: self) - }() - - public lazy var position: ReactiveProperty = { - let layer = self.layer - return createCoreAnimationProperty(#function, - initialValue: layer.position, - externalWrite: { layer.position = $0 }, - keyPath: "position", - reactiveLayer: self) - }() - - public lazy var positionX: ReactiveProperty = { - let position = self.position - return createCoreAnimationProperty(#function, - initialValue: position.value.x, - externalWrite: { var point = position.value; point.x = $0; position.value = point }, - keyPath: "position.x", - reactiveLayer: self) - }() - - public lazy var positionY: ReactiveProperty = { - let position = self.position - return createCoreAnimationProperty(#function, - initialValue: position.value.y, - externalWrite: { var point = position.value; point.y = $0; position.value = point }, - keyPath: "position.y", - reactiveLayer: self) - }() - - public lazy var size: ReactiveProperty = { - let layer = self.layer - return createCoreAnimationProperty(#function, - initialValue: layer.bounds.size, - externalWrite: { layer.bounds.size = $0 }, - keyPath: "bounds.size", - reactiveLayer: self) - }() - - public lazy var width: ReactiveProperty = { - let size = self.size - return createCoreAnimationProperty(#function, - initialValue: size.value.width, - externalWrite: { var dimensions = size.value; dimensions.width = $0; size.value = dimensions }, - keyPath: "bounds.size.width", - reactiveLayer: self) - }() - - public lazy var height: ReactiveProperty = { - let size = self.size - return createCoreAnimationProperty(#function, - initialValue: size.value.height, - externalWrite: { var dimensions = size.value; dimensions.height = $0; size.value = dimensions }, - keyPath: "bounds.size.height", - reactiveLayer: self) - }() - - public lazy var anchorPoint: ReactiveProperty = { - let layer = self.layer - return createCoreAnimationProperty(#function, - initialValue: layer.anchorPoint, - externalWrite: { layer.anchorPoint = $0 }, - keyPath: "anchorPoint", - reactiveLayer: self) - }() - - public lazy var anchorPointAdjustment: ReactiveProperty = { - let anchorPoint = self.anchorPoint - let position = self.position - let layer = self.layer - return ReactiveProperty(#function, - initialValue: .init(anchorPoint: anchorPoint.value, position: position.value), - externalWrite: { anchorPoint.value = $0.anchorPoint; position.value = $0.position }) - }() - - public lazy var rotation: ReactiveProperty = { - let layer = self.layer - return createCoreAnimationProperty(#function, - initialValue: layer.value(forKeyPath: "transform.rotation.z") as! CGFloat, - externalWrite: { layer.setValue($0, forKeyPath: "transform.rotation.z") }, - keyPath: "transform.rotation.z", - reactiveLayer: self) - }() - - public lazy var scale: ReactiveProperty = { - let layer = self.layer - return createCoreAnimationProperty(#function, - initialValue: layer.value(forKeyPath: "transform.scale") as! CGFloat, - externalWrite: { layer.setValue($0, forKeyPath: "transform.scale") }, - keyPath: "transform.scale.xy", - reactiveLayer: self) - }() - - public lazy var shadowPath: ReactiveProperty = { - let layer = self.layer - return createCoreAnimationProperty(#function, - initialValue: layer.shadowPath!, - externalWrite: { layer.shadowPath = $0 }, - keyPath: "shadowPath", - reactiveLayer: self) - }() - - fileprivate var timeline: Timeline? { - didSet { - if oldValue === timeline { - return - } - guard let timeline = timeline else { - timelineSubscription = nil - return - } - - timelineSubscription = timeline.subscribeToValue { [weak self] state in - guard let strongSelf = self else { return } - strongSelf.lastTimelineState = state - - if state.paused { - strongSelf.layer.speed = 0 - strongSelf.layer.timeOffset = TimeInterval(state.beginTime + state.timeOffset) - - } else if strongSelf.layer.speed == 0 { // Unpause the layer. - // The following logic is the magic sauce required to reconnect a CALayer with the - // render server's clock. - let pausedTime = strongSelf.layer.timeOffset - strongSelf.layer.speed = 1 - strongSelf.layer.timeOffset = 0 - strongSelf.layer.beginTime = 0 - let timeSincePause = strongSelf.layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime - strongSelf.layer.beginTime = timeSincePause - } - } - } - } - fileprivate var decomposedKeys = Set() - fileprivate var lastTimelineState: Timeline.Snapshot? - private var timelineSubscription: Subscription? - - init(_ layer: CALayer) { - self.layer = layer - } -} - -/** - Creates a Core Animation-compatible reactive property instance. - */ -public func createCoreAnimationProperty(_ name: String, initialValue: T, externalWrite: @escaping NextChannel, keyPath: String, reactiveLayer: ReactiveCALayer) -> ReactiveProperty { - let layer = reactiveLayer.layer - let property = ReactiveProperty("\(pretty(reactiveLayer)).\(name)", initialValue: initialValue, externalWrite: { value in - let actionsWereDisabled = CATransaction.disableActions() - CATransaction.setDisableActions(true) - externalWrite(value) - CATransaction.setDisableActions(actionsWereDisabled) - }, coreAnimation: { [weak reactiveLayer] event in - guard let strongReactiveLayer = reactiveLayer else { return } - switch event { - case .add(let info): - if let timeline = info.timeline { - strongReactiveLayer.timeline = timeline - } - - let animation = info.animation.copy() as! CAPropertyAnimation - - animation.duration *= TimeInterval(simulatorDragCoefficient()) - - if layer.speed == 0, let lastTimelineState = strongReactiveLayer.lastTimelineState { - animation.beginTime = TimeInterval(lastTimelineState.beginTime) + animation.beginTime - } else { - animation.beginTime = layer.convertTime(CACurrentMediaTime(), from: nil) + animation.beginTime - } - - animation.keyPath = keyPath - - if let unsafeMakeAdditive = info.makeAdditive { - let makeAdditive: ((Any, Any) -> Any) = { from, to in - // When mapping properties to properties it's possible for the values to get implicitly - // wrapped in an NSNumber instance. This may cause the generic makeAdditive - // implementation to fail to cast to T, so we unbox the type here instead. - if let from = from as? NSNumber, let to = to as? NSNumber { - return from.doubleValue - to.doubleValue - } - return unsafeMakeAdditive(from, to) - } - - if let basicAnimation = animation as? CABasicAnimation { - basicAnimation.fromValue = makeAdditive(basicAnimation.fromValue!, basicAnimation.toValue!) - basicAnimation.toValue = makeAdditive(basicAnimation.toValue!, basicAnimation.toValue!) - basicAnimation.isAdditive = true - - } else if let keyframeAnimation = animation as? CAKeyframeAnimation { - let lastValue = keyframeAnimation.values!.last! - keyframeAnimation.values = keyframeAnimation.values!.map { makeAdditive($0, lastValue) } - keyframeAnimation.isAdditive = true - } - } - - // Core Animation springs do not support multi-dimensional velocity, so we bear the burden - // of decomposing multi-dimensional springs here. - if let springAnimation = animation as? CASpringAnimation - , springAnimation.isAdditive - , let initialVelocity = info.initialVelocity as? CGPoint - , let delta = springAnimation.fromValue as? CGPoint { - let decomposed = decompose(springAnimation: springAnimation, - delta: delta, - initialVelocity: initialVelocity) - - CATransaction.begin() - CATransaction.setCompletionBlock(info.onCompletion) - layer.add(decomposed.0, forKey: info.key + ".x") - layer.add(decomposed.1, forKey: info.key + ".y") - CATransaction.commit() - - strongReactiveLayer.decomposedKeys.insert(info.key) - return - } - - if let initialVelocity = info.initialVelocity { - applyInitialVelocity(initialVelocity, to: animation) - } - - CATransaction.begin() - CATransaction.setCompletionBlock(info.onCompletion) - layer.add(animation, forKey: info.key + "." + keyPath) - CATransaction.commit() - - case .remove(let key): - if let presentationLayer = layer.presentation() { - layer.setValue(presentationLayer.value(forKeyPath: keyPath), forKeyPath: keyPath) - } - if strongReactiveLayer.decomposedKeys.contains(key) { - layer.removeAnimation(forKey: key + ".x") - layer.removeAnimation(forKey: key + ".y") - strongReactiveLayer.decomposedKeys.remove(key) - - } else { - layer.removeAnimation(forKey: key + "." + keyPath) - } - } - }) - var lastView: UIView? - property.shouldVisualizeMotion = { view, containerView in - if lastView != view, let lastView = lastView { - lastView.removeFromSuperview() - } - view.isUserInteractionEnabled = false - if let superlayer = layer.superlayer { - view.frame = superlayer.convert(superlayer.bounds, to: containerView.layer) - } else { - view.frame = containerView.bounds - } - containerView.addSubview(view) - lastView = view - } - - return property -} - -public class ReactiveCAShapeLayer: ReactiveCALayer { - public let shapeLayer: CAShapeLayer - - /** A property representing the layer's .path value. */ - public lazy var path: ReactiveProperty = { - let layer = self.shapeLayer - return createCoreAnimationProperty(#function, - initialValue: layer.path!, - externalWrite: { layer.path = $0 }, - keyPath: "path", - reactiveLayer: self) - }() - - init(_ shapeLayer: CAShapeLayer) { - self.shapeLayer = shapeLayer - super.init(shapeLayer) - } -} - -private func decompose(springAnimation: CASpringAnimation, delta: CGPoint, initialVelocity: CGPoint) -> (CASpringAnimation, CASpringAnimation) { - let xAnimation = springAnimation.copy() as! CASpringAnimation - let yAnimation = springAnimation.copy() as! CASpringAnimation - xAnimation.fromValue = delta.x - yAnimation.fromValue = delta.y - xAnimation.toValue = 0 - yAnimation.toValue = 0 - - if delta.x != 0 { - xAnimation.initialVelocity = initialVelocity.x / -delta.x - } - if delta.y != 0 { - yAnimation.initialVelocity = initialVelocity.y / -delta.y - } - - xAnimation.keyPath = springAnimation.keyPath! + ".x" - yAnimation.keyPath = springAnimation.keyPath! + ".y" - - return (xAnimation, yAnimation) -} - -private func applyInitialVelocity(_ initialVelocity: Any, to animation: CAPropertyAnimation) { - if let springAnimation = animation as? CASpringAnimation, springAnimation.isAdditive { - // Additive animations have a toValue of 0 and a fromValue of negative delta (where the model - // value came from). - guard let initialVelocity = initialVelocity as? CGFloat, let delta = springAnimation.fromValue as? CGFloat else { - // Unsupported velocity type. - return - } - if delta != 0 { - // CASpringAnimation's initialVelocity is proportional to the distance to travel, i.e. our - // delta. - springAnimation.initialVelocity = initialVelocity / -delta - } - } -} diff --git a/src/reactivetypes/ReactiveUIView.swift b/src/reactivetypes/ReactiveUIView.swift deleted file mode 100644 index f36ff34..0000000 --- a/src/reactivetypes/ReactiveUIView.swift +++ /dev/null @@ -1,53 +0,0 @@ -/* - Copyright 2016-present The Material Motion Authors. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import UIKit - -public class ReactiveUIView { - public let view: UIView - - public lazy var isUserInteractionEnabled: ReactiveProperty = { - let view = self.view - return ReactiveProperty("\(pretty(view)).\(#function)", - initialValue: view.isUserInteractionEnabled, - externalWrite: { view.isUserInteractionEnabled = $0 }) - }() - - public lazy var backgroundColor: ReactiveProperty = { - let view = self.view - return ReactiveProperty("\(pretty(view)).\(#function)", - initialValue: view.backgroundColor!, - externalWrite: { view.backgroundColor = $0 }) - }() - - public lazy var alpha: ReactiveProperty = { - let view = self.view - return ReactiveProperty("\(pretty(view)).\(#function)", - initialValue: view.alpha, - externalWrite: { view.alpha = $0 }) - }() - - public lazy var reactiveLayer: ReactiveCALayer = { - return self.runtime?.get(self.view.layer) ?? ReactiveCALayer(self.view.layer) - }() - - init(_ view: UIView, runtime: MotionRuntime) { - self.view = view - self.runtime = runtime - } - - private weak var runtime: MotionRuntime? -} diff --git a/src/timeline/CALayer+Timeline.swift b/src/timeline/CALayer+Timeline.swift new file mode 100644 index 0000000..914f15a --- /dev/null +++ b/src/timeline/CALayer+Timeline.swift @@ -0,0 +1,68 @@ +/* + Copyright 2016-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import IndefiniteObservable +import UIKit + +extension CALayer { + private class TimelineInfo { + var timeline: Timeline? + var lastState: Timeline.Snapshot? + var subscription: Subscription? + } + + private struct AssociatedKeys { + static var timelineInfo = "MDMTimelineInfo" + } + + var timeline: Timeline? { + get { return (objc_getAssociatedObject(self, &AssociatedKeys.timelineInfo) as? TimelineInfo)?.timeline } + set { + let timelineInfo = (objc_getAssociatedObject(self, &AssociatedKeys.timelineInfo) as? TimelineInfo) ?? TimelineInfo() + timelineInfo.timeline = newValue + objc_setAssociatedObject(self, &AssociatedKeys.timelineInfo, timelineInfo, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + + guard let timeline = timelineInfo.timeline else { + timelineInfo.subscription = nil + return + } + + timelineInfo.subscription = timeline.subscribeToValue { [weak self] state in + guard let strongSelf = self else { return } + timelineInfo.lastState = state + + if state.paused { + strongSelf.speed = 0 + strongSelf.timeOffset = TimeInterval(state.beginTime + state.timeOffset) + + } else if strongSelf.speed == 0 { // Unpause the layer. + // The following logic is the magic sauce required to reconnect a CALayer with the + // render server's clock. + let pausedTime = strongSelf.timeOffset + strongSelf.speed = 1 + strongSelf.timeOffset = 0 + strongSelf.beginTime = 0 + let timeSincePause = strongSelf.convertTime(CACurrentMediaTime(), from: nil) - pausedTime + strongSelf.beginTime = timeSincePause + } + } + } + } + + var lastTimelineState: Timeline.Snapshot? { + return (objc_getAssociatedObject(self, &AssociatedKeys.timelineInfo) as? TimelineInfo)?.lastState + } +} diff --git a/tests/unit/MotionRuntimeTests.swift b/tests/unit/MotionRuntimeTests.swift index 58a2d8f..6dd9b8f 100644 --- a/tests/unit/MotionRuntimeTests.swift +++ b/tests/unit/MotionRuntimeTests.swift @@ -28,7 +28,7 @@ class MotionRuntimeTests: XCTestCase { let reactiveShapeLayer = runtime.get(shapeLayer) let reactiveCastedLayer = runtime.get(castedLayer) - XCTAssertTrue(reactiveShapeLayer === reactiveCastedLayer) + XCTAssertTrue(reactiveShapeLayer._properties === reactiveCastedLayer._properties) } func testInteractionsReturnsEmptyArrayWithoutAnyAddedInteractions() { diff --git a/tests/unit/ReactivePropertyTests.swift b/tests/unit/ReactivePropertyTests.swift index 328a9d5..b51b12c 100644 --- a/tests/unit/ReactivePropertyTests.swift +++ b/tests/unit/ReactivePropertyTests.swift @@ -113,4 +113,72 @@ class ReactivePropertyTests: XCTestCase { waitForExpectations(timeout: 0) } + + // MARK: Reactive objects + + func testReactivePropertyInstancesAreIdenticalAcrossInstances() { + let view = UIView() + XCTAssertTrue(Reactive(view).isUserInteractionEnabled === Reactive(view).isUserInteractionEnabled) + } + + func testPropertiesReleasedWhenDereferenced() { + let view = UIView() + var prop1: ReactiveProperty? = Reactive(view).isUserInteractionEnabled + let objectIdentifier = ObjectIdentifier(prop1!) + + prop1 = nil + + let prop2 = Reactive(view).isUserInteractionEnabled + XCTAssertTrue(objectIdentifier != ObjectIdentifier(prop2)) + } + + func testObjectRetainedByReactiveType() { + var reactive: Reactive? + weak var weakView: UIView? + + autoreleasepool { + let view = UIView() + weakView = view + reactive = Reactive(view) + } + + XCTAssertNotNil(weakView) + XCTAssertNotNil(reactive) + } + + func testObjectReleasedWhenReactiveTypeReleased() { + var reactive: Reactive? + weak var weakView: UIView? + + let allocate = { + let view = UIView() + weakView = view + reactive = Reactive(view) + } + allocate() + + reactive = nil + + XCTAssertNil(weakView) + + // Resolve compiler warning about not reading reactive after writing to it. + XCTAssertNil(reactive) + } + + func testReactiveObjectNotGloballyRetained() { + let view = UIView() + weak var weakReactive: Reactive? = Reactive(view) + + XCTAssertNil(weakReactive) + } + + func testObjectNotGloballyRetained() { + var view: UIView? = UIView() + weak var weakView: UIView? = view + let _ = Reactive(view!) + + view = nil + + XCTAssertNil(weakView) + } } From ffc8f8c7d3557587721db5e2f1cff0d37ceee0f5 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 28 Apr 2017 17:35:52 -0400 Subject: [PATCH 22/60] Mark all MotionObservable subscribe methods with @discardableResult. Summary: We no longer need to store Subscriptions since upgrading to IndefiniteObservable v4. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, appsforartists Reviewed By: O2 Material Motion, #material_motion, appsforartists Tags: #material_motion Differential Revision: http://codereview.cc/D3136 --- src/reactivetypes/MotionObservable.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/reactivetypes/MotionObservable.swift b/src/reactivetypes/MotionObservable.swift index ee9a243..f1fd910 100644 --- a/src/reactivetypes/MotionObservable.swift +++ b/src/reactivetypes/MotionObservable.swift @@ -184,6 +184,7 @@ extension MotionObservableConvertible { /** Sugar for subscribing a MotionObserver. */ + @discardableResult public func subscribe(next: @escaping NextChannel, coreAnimation: @escaping CoreAnimationChannel, visualization: @escaping VisualizationChannel) -> Subscription { @@ -196,6 +197,7 @@ extension MotionObservableConvertible { Forwards all channel values to the provided observer except next, which is provided as an argument. */ + @discardableResult public func subscribeAndForward(to observer: MotionObserver, next: @escaping NextChannel) -> Subscription { return subscribe(next: next, coreAnimation: { event in observer.coreAnimation?(event) }, @@ -205,6 +207,7 @@ extension MotionObservableConvertible { /** Forwards all channel values to the provided observer. */ + @discardableResult public func subscribeAndForward(to observer: MotionObserver) -> Subscription { return asStream().subscribe(observer: observer) } @@ -212,6 +215,7 @@ extension MotionObservableConvertible { /** Subscribes only to the value channel of the stream. */ + @discardableResult public func subscribeToValue(_ next: @escaping NextChannel) -> Subscription { return asStream().subscribe(observer: MotionObserver(next: next)) } From db2d5bc8540b282e7dcc72271db62ff3d9565071 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 28 Apr 2017 17:40:20 -0400 Subject: [PATCH 23/60] Add a ReactiveScrollViewDelegate and replace usage of the MotionRuntime in the carousel demo. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3137 --- examples/CarouselExample.swift | 31 ++++------ .../ReactiveScrollViewDelegate.swift | 61 +++++++++++++++++++ 2 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 src/reactivetypes/ReactiveScrollViewDelegate.swift diff --git a/examples/CarouselExample.swift b/examples/CarouselExample.swift index e020268..77c87bf 100644 --- a/examples/CarouselExample.swift +++ b/examples/CarouselExample.swift @@ -19,7 +19,7 @@ import MaterialMotion class CarouselExampleViewController: ExampleViewController, UIScrollViewDelegate { - var runtime: MotionRuntime! + let delegate = ReactiveScrollViewDelegate() override func viewDidLoad() { super.viewDidLoad() @@ -29,10 +29,10 @@ class CarouselExampleViewController: ExampleViewController, UIScrollViewDelegate scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight] scrollView.isPagingEnabled = true scrollView.contentSize = .init(width: view.bounds.size.width * 3, height: view.bounds.size.height) - scrollView.delegate = self + scrollView.delegate = delegate view.addSubview(scrollView) - pager = UIPageControl() + let pager = UIPageControl() let size = pager.sizeThatFits(view.bounds.size) pager.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] pager.frame = .init(x: 0, y: view.bounds.height - size.height - 20, width: view.bounds.width, height: size.height) @@ -45,9 +45,6 @@ class CarouselExampleViewController: ExampleViewController, UIScrollViewDelegate (title: "Page 3", description: "Page 3 description", color: .secondaryColor), ] - runtime = MotionRuntime(containerView: view) - - let stream = runtime.get(scrollView) for (index, data) in datas.enumerated() { let page = CarouselPage(frame: view.bounds) page.frame.origin.x = CGFloat(index) * view.bounds.width @@ -56,21 +53,19 @@ class CarouselExampleViewController: ExampleViewController, UIScrollViewDelegate page.iconView.backgroundColor = data.color scrollView.addSubview(page) - let pageEdge = stream.x().offset(by: -page.frame.origin.x) + let pageEdge = delegate.x().offset(by: -page.frame.origin.x) - runtime.connect(pageEdge.rewriteRange(start: 0, end: 128, - destinationStart: 1, destinationEnd: 0), - to: runtime.get(page).alpha) - runtime.connect(pageEdge.rewriteRange(start: -view.bounds.width, end: 0, - destinationStart: 0.5, destinationEnd: 1.0), - to: runtime.get(page.layer).scale) + pageEdge.rewriteRange(start: 0, end: 128, destinationStart: 1, destinationEnd: 0).subscribeToValue { + page.alpha = $0 + } + pageEdge.rewriteRange(start: -view.bounds.width, end: 0, destinationStart: 0.5, destinationEnd: 1.0).subscribeToValue { + Reactive(page.layer).scale.value = $0 + } } - } - var pager: UIPageControl! - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - pager.currentPage = Int((scrollView.contentOffset.x + scrollView.bounds.width / 2) / scrollView.bounds.width) + delegate.x().offset(by: scrollView.bounds.width / 2).scaled(by: 1 / scrollView.bounds.width).subscribeToValue { + pager.currentPage = Int($0) + } } override func exampleInformation() -> ExampleInfo { diff --git a/src/reactivetypes/ReactiveScrollViewDelegate.swift b/src/reactivetypes/ReactiveScrollViewDelegate.swift new file mode 100644 index 0000000..6a35ded --- /dev/null +++ b/src/reactivetypes/ReactiveScrollViewDelegate.swift @@ -0,0 +1,61 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/** + A UIScrollViewDelegate implementation that exposes observable streams for the scroll view delegate + events. + + Supported events: + + - scrollViewDidScroll: + + The canonical stream will emit the contentOffset each time a scrollViewDidScroll event is received. + */ +public final class ReactiveScrollViewDelegate: NSObject, UIScrollViewDelegate, MotionObservableConvertible { + public override init() { + super.init() + } + + // MARK: Canonical stream + + public func asStream() -> MotionObservable { + return didScroll._map { $0.contentOffset } + } + + // MARK: Streams + + public var didScroll: MotionObservable { + return MotionObservable { observer in + self.didScrollObservers.append(observer) + return { + if let index = self.didScrollObservers.index(where: { $0 === observer }) { + self.didScrollObservers.remove(at: index) + } + } + } + } + + // MARK: UIScrollViewDelegate + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + didScrollObservers.forEach { $0.next(scrollView) } + } + private var didScrollObservers: [MotionObserver] = [] + + public let metadata = Metadata("Scroll view delegate") +} From 98065f2504dec4da267e6f60c7bdd3b7809b3215 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 1 May 2017 13:12:27 -0400 Subject: [PATCH 24/60] Fix build failure. --- tests/unit/MotionRuntimeTests.swift | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/unit/MotionRuntimeTests.swift b/tests/unit/MotionRuntimeTests.swift index 6dd9b8f..3280def 100644 --- a/tests/unit/MotionRuntimeTests.swift +++ b/tests/unit/MotionRuntimeTests.swift @@ -17,6 +17,17 @@ import XCTest import MaterialMotion +public class MockTween: Togglable, Stateful { + let _state = createProperty("Tween._state", withInitialValue: MotionState.atRest) + public let enabled = createProperty("Tween.enabled", withInitialValue: true) + public func setState(state: MotionState) { + _state.value = state + } + public var state: MotionObservable { + return _state.asStream() + } +} + class MotionRuntimeTests: XCTestCase { func testReactiveObjectCacheSupportsSubclassing() { @@ -49,17 +60,6 @@ class MotionRuntimeTests: XCTestCase { XCTAssertEqual(results.count, 1) } - public class MockTween: Togglable, Stateful { - let _state = createProperty("Tween._state", withInitialValue: MotionState.atRest) - public let enabled = createProperty("Tween.enabled", withInitialValue: true) - public func setState(state: MotionState) { - _state.value = state - } - public var state: MotionObservable { - return _state.asStream() - } - } - func testRuntimeStart() { let promise = expectation(description: "start interaction B when interaction A is at state") From 7fe07b0f1e3d599aba8727645367df0b553df529 Mon Sep 17 00:00:00 2001 From: Eric Tang Date: Tue, 2 May 2017 16:15:36 -0400 Subject: [PATCH 25/60] Swap params for runtime.interactions() API Summary: Swap params for runtime.interactions() to improve clarity. Fixes https://github.com/material-motion/material-motion-swift/issues/117 Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei, featherless Reviewed By: markwei Subscribers: featherless Tags: #material_motion Differential Revision: http://codereview.cc/D3141 --- src/MotionRuntime.swift | 12 ++++++++++-- tests/unit/MotionRuntimeTests.swift | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift index 83850c8..305037b 100644 --- a/src/MotionRuntime.swift +++ b/src/MotionRuntime.swift @@ -84,8 +84,16 @@ public final class MotionRuntime { Example usage: - let draggables = runtime.interactions(for: view, type: Draggable.self) + let draggables = runtime.interactions(ofType: Draggable.self, for: view) */ + public func interactions(ofType: I.Type, for target: I.Target) -> [I] where I: Interaction, I.Target: AnyObject { + guard let interactions = targets[ObjectIdentifier(target)] else { + return [] + } + return interactions.flatMap { $0 as? I } + } + + @available(*, deprecated, message: "Use interactions(ofType:for:) instead.") public func interactions(for target: I.Target, ofType: I.Type) -> [I] where I: Interaction, I.Target: AnyObject { guard let interactions = targets[ObjectIdentifier(target)] else { return [] @@ -93,7 +101,7 @@ public final class MotionRuntime { return interactions.flatMap { $0 as? I } } - @available(*, deprecated, message: "Use interactions(for:ofType:) instead.") + @available(*, deprecated, message: "Use interactions(ofType:for:) instead.") public func interactions(for target: I.Target, filter: (Any) -> I?) -> [I] where I: Interaction, I.Target: AnyObject { guard let interactions = targets[ObjectIdentifier(target)] else { return [] diff --git a/tests/unit/MotionRuntimeTests.swift b/tests/unit/MotionRuntimeTests.swift index 3280def..268225e 100644 --- a/tests/unit/MotionRuntimeTests.swift +++ b/tests/unit/MotionRuntimeTests.swift @@ -45,7 +45,7 @@ class MotionRuntimeTests: XCTestCase { func testInteractionsReturnsEmptyArrayWithoutAnyAddedInteractions() { let runtime = MotionRuntime(containerView: UIView()) - let results = runtime.interactions(for: UIView(), ofType: Draggable.self) + let results = runtime.interactions(ofType: Draggable.self, for: UIView()) XCTAssertEqual(results.count, 0) } @@ -56,7 +56,7 @@ class MotionRuntimeTests: XCTestCase { runtime.add(Draggable(), to: view) runtime.add(Rotatable(), to: view) - let results = runtime.interactions(for: view, ofType: Draggable.self) + let results = runtime.interactions(ofType: Draggable.self, for: view) XCTAssertEqual(results.count, 1) } From cb8ba4ff8387d688e47f84cd3e5980ef6ab8d030 Mon Sep 17 00:00:00 2001 From: Eric Tang Date: Wed, 3 May 2017 10:50:51 -0400 Subject: [PATCH 26/60] Added unit test Summary: Test getting interactions from properties such as opacity, scale, and etc. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, featherless Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, featherless Tags: #material_motion Differential Revision: http://codereview.cc/D3152 --- tests/unit/MotionRuntimeTests.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unit/MotionRuntimeTests.swift b/tests/unit/MotionRuntimeTests.swift index 268225e..45c0722 100644 --- a/tests/unit/MotionRuntimeTests.swift +++ b/tests/unit/MotionRuntimeTests.swift @@ -60,6 +60,23 @@ class MotionRuntimeTests: XCTestCase { XCTAssertEqual(results.count, 1) } + func testReturnsInteractionsOfProperties() { + let runtime = MotionRuntime(containerView: UIView()) + + let view = UIView() + let tweenA = Tween(duration: 1, values: [1, 0, 1]) + runtime.add(tweenA, to: runtime.get(view.layer).opacity) + + let tweenB = Tween(duration: 1, values: [1.3]) + runtime.add(tweenB, to: runtime.get(view.layer).scale) + + var results = runtime.interactions(ofType: Tween.self, for: runtime.get(view.layer).opacity) + XCTAssertEqual(results.count, 1) + + results = runtime.interactions(ofType: Tween.self, for: runtime.get(view.layer).scale) + XCTAssertEqual(results.count, 1) + } + func testRuntimeStart() { let promise = expectation(description: "start interaction B when interaction A is at state") From 27edf6b1f12c2587fa096c22ed9d3d484fc0ca39 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Mon, 1 May 2017 13:17:04 -0400 Subject: [PATCH 27/60] Remove use of Reactive type in carousel example. Summary: Further simplifying the carousel example. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3139 --- examples/CarouselExample.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/CarouselExample.swift b/examples/CarouselExample.swift index 77c87bf..5a6da76 100644 --- a/examples/CarouselExample.swift +++ b/examples/CarouselExample.swift @@ -59,7 +59,7 @@ class CarouselExampleViewController: ExampleViewController, UIScrollViewDelegate page.alpha = $0 } pageEdge.rewriteRange(start: -view.bounds.width, end: 0, destinationStart: 0.5, destinationEnd: 1.0).subscribeToValue { - Reactive(page.layer).scale.value = $0 + page.layer.transform = CATransform3DMakeScale($0, $0, 1) } } From 86bf12b91ee6d2f1857160a30582221a8a9c4ce7 Mon Sep 17 00:00:00 2001 From: Eric Tang Date: Fri, 5 May 2017 14:08:44 -0400 Subject: [PATCH 28/60] Added ignoreUntil and simplified slop Summary: Fixed https://github.com/material-motion/material-motion-swift/issues/87 and https://github.com/material-motion/material-motion-swift/issues/86 Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, featherless Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, featherless Subscribers: featherless Tags: #material_motion Differential Revision: http://codereview.cc/D3170 --- .../project.pbxproj | 4 ++ src/operators/ignoreUntil.swift | 33 ++++++++++++++++ src/operators/slop.swift | 15 +------ tests/unit/operator/ignoreUntilTest.swift | 39 +++++++++++++++++++ 4 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 src/operators/ignoreUntil.swift create mode 100644 tests/unit/operator/ignoreUntilTest.swift diff --git a/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj index 1f859f2..872aeaa 100644 --- a/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 07EB59021EBCEF8F0045ABAE /* ignoreUntilTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07EB59001EBCEF3D0045ABAE /* ignoreUntilTest.swift */; }; 19940C71B8C97EDA48552251 /* Pods_MaterialMotionCatalog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16DDCA39C49FF5C091B2AF6C /* Pods_MaterialMotionCatalog.framework */; }; 6605044E1E83146C009EDB8A /* distanceFromTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6605044D1E83146C009EDB8A /* distanceFromTests.swift */; }; 66090B841E03715F00B1D598 /* PropertyObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66090B831E03715F00B1D598 /* PropertyObservation.swift */; }; @@ -87,6 +88,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 07EB59001EBCEF3D0045ABAE /* ignoreUntilTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ignoreUntilTest.swift; sourceTree = ""; }; 1486CD16AAF554D1E8B5BBB3 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = ""; }; 16DDCA39C49FF5C091B2AF6C /* Pods_MaterialMotionCatalog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MaterialMotionCatalog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F2DC52F25787C67CDBB6189 /* Pods-UnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.release.xcconfig"; sourceTree = ""; }; @@ -276,6 +278,7 @@ 669512921E8301D100D8868D /* dedupeTests.swift */, 669512961E8305AC00D8868D /* delayTests.swift */, 6605044D1E83146C009EDB8A /* distanceFromTests.swift */, + 07EB59001EBCEF3D0045ABAE /* ignoreUntilTest.swift */, 66F2C3C81E83245800DD9728 /* invertedTests.swift */, 6695129A1E830AE500D8868D /* lowerBoundTests.swift */, 6613A9DA1E832779004A3699 /* mergeTests.swift */, @@ -653,6 +656,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 07EB59021EBCEF8F0045ABAE /* ignoreUntilTest.swift in Sources */, 6613A9F51E842FF5004A3699 /* yLockedToTests.swift in Sources */, 669512891E82E8CB00D8868D /* _rememberTests.swift in Sources */, 669512741E82E68900D8868D /* SubtractableTests.swift in Sources */, diff --git a/src/operators/ignoreUntil.swift b/src/operators/ignoreUntil.swift new file mode 100644 index 0000000..caf488b --- /dev/null +++ b/src/operators/ignoreUntil.swift @@ -0,0 +1,33 @@ +/* + Copyright 2016-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +extension MotionObservableConvertible where T: Equatable { + /** + Ignores values from upstream until the expected value is received, at which point it emits + that value and all further values without modification. + */ + public func ignoreUntil(_ expected: T) -> MotionObservable { + var shouldSend = false + return _filter() { value in + if value == expected { + shouldSend = true + } + return shouldSend + } + } +} diff --git a/src/operators/slop.swift b/src/operators/slop.swift index 5e3aa54..6a94f9f 100644 --- a/src/operators/slop.swift +++ b/src/operators/slop.swift @@ -48,28 +48,17 @@ extension MotionObservableConvertible where T == CGFloat { size. */ public func slop(size: CGFloat) -> MotionObservable { - let didLeaveSlopRegion = createProperty("slop.didLeaveSlopRegion", withInitialValue: false) - let size = abs(size) return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [size]))) { observer in let upstreamSubscription = self - .thresholdRange(min: -size, max: size) - .rewrite([.below: true, .above: true]) - .dedupe() - .subscribeToValue { didLeaveSlopRegion.value = $0 } - - let downstreamSubscription = self - .valve(openWhenTrue: didLeaveSlopRegion) .thresholdRange(min: -size, max: size) .rewrite([.below: .onExit, .within: .onReturn, .above: .onExit]) .dedupe() + .ignoreUntil(SlopEvent.onExit) .subscribeAndForward(to: observer) - return { - upstreamSubscription.unsubscribe() - downstreamSubscription.unsubscribe() - } + return upstreamSubscription.unsubscribe } } } diff --git a/tests/unit/operator/ignoreUntilTest.swift b/tests/unit/operator/ignoreUntilTest.swift new file mode 100644 index 0000000..e2b84ec --- /dev/null +++ b/tests/unit/operator/ignoreUntilTest.swift @@ -0,0 +1,39 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import XCTest +import IndefiniteObservable +import MaterialMotion + +class ignoreUntilTest: XCTestCase { + func testIgnoreUntil() { + + let input = [20, 10, 60, 50, 10, 20, 80] + let expected = [50, 10, 20, 80] + let observable = MotionObservable { observer in + for i in input { + observer.next(i) + } + return noopDisconnect + } + + var values: [Int] = [] + observable.ignoreUntil(50).subscribeToValue { + values.append($0) + } + XCTAssertEqual(values, expected) + } +} From 7b5e51f534a206f054b40a9a61a1bdd03a8da957 Mon Sep 17 00:00:00 2001 From: Brenton Simpson Date: Mon, 8 May 2017 23:53:42 -0700 Subject: [PATCH 29/60] Add Discord badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4f91cda..d211161 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/MaterialMotion.svg)](https://cocoapods.org/pods/MaterialMotion) [![Platform](https://img.shields.io/cocoapods/p/MaterialMotion.svg)](http://cocoadocs.org/docsets/MaterialMotion) [![Docs](https://img.shields.io/cocoapods/metrics/doc-percent/MaterialMotion.svg)](http://cocoadocs.org/docsets/MaterialMotion) +[![Chat](https://img.shields.io/discord/198544450366996480.svg)](https://discord.gg/material-motion) This library includes a variety of ready-to-use **interactions**. Interactions are registered to an instance of `MotionRuntime`: From 33f60d7b260f3c4b9f0c8ad64230bfd68d643e71 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 9 May 2017 14:47:39 -0400 Subject: [PATCH 30/60] Avoid excessive TransitionTween emissions when the transition direction changes. Summary: Was noticing that TransitionTween was re-emitting animations every time the transition direction was written to, even if it didn't change. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, appsforartists Reviewed By: O2 Material Motion, #material_motion, appsforartists Tags: #material_motion Differential Revision: http://codereview.cc/D3185 --- src/interactions/TransitionTween.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/interactions/TransitionTween.swift b/src/interactions/TransitionTween.swift index 745821a..6b4048d 100644 --- a/src/interactions/TransitionTween.swift +++ b/src/interactions/TransitionTween.swift @@ -68,8 +68,8 @@ public final class TransitionTween: Tween { self.direction = direction - self.toggledValues = direction.rewrite([.backward: backwardValues, .forward: forwardValues]) - self.toggledKeyPositions = direction.rewrite([.backward: backwardKeyPositions, .forward: forwardKeyPositions]) + self.toggledValues = direction.dedupe().rewrite([.backward: backwardValues, .forward: forwardValues]) + self.toggledKeyPositions = direction.dedupe().rewrite([.backward: backwardKeyPositions, .forward: forwardKeyPositions]) super.init(duration: duration, values: values, system: system, timeline: timeline) } @@ -92,7 +92,7 @@ public final class TransitionTween: Tween { withRuntime runtime: MotionRuntime, constraints: ConstraintApplicator? = nil) { let unlocked = createProperty("TransitionTween.unlocked", withInitialValue: false) - runtime.connect(direction.rewriteTo(false), to: unlocked) + runtime.connect(direction.dedupe().rewriteTo(false), to: unlocked) runtime.connect(toggledValues, to: values) runtime.connect(toggledKeyPositions, to: keyPositions) super.add(to: property, withRuntime: runtime) { @@ -102,7 +102,7 @@ public final class TransitionTween: Tween { } return stream.valve(openWhenTrue: unlocked) } - runtime.connect(direction.rewriteTo(true), to: unlocked) + runtime.connect(direction.dedupe().rewriteTo(true), to: unlocked) } private let direction: ReactiveProperty From d93233e8e433592d6445da9a5bfc382d5f622f44 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 9 May 2017 14:34:42 -0400 Subject: [PATCH 31/60] Add support for customizing transition presentation. Summary: Transitions can now conform to TransitionWithPresentation in order to return a custom presentation controller. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3196 --- src/transitions/Transition.swift | 40 ++++++++++++++-- src/transitions/TransitionContext.swift | 48 ++++++-------------- src/transitions/TransitionController.swift | 53 ++++++++++++++++++---- 3 files changed, 91 insertions(+), 50 deletions(-) diff --git a/src/transitions/Transition.swift b/src/transitions/Transition.swift index b36654a..c61f43f 100644 --- a/src/transitions/Transition.swift +++ b/src/transitions/Transition.swift @@ -18,12 +18,9 @@ import Foundation import UIKit /** - A transition is responsible for describing the motion that will occur during a UIViewController - transition. + An object that is capable of responding to a transition that is about to begin. */ -public protocol Transition { - /** Transition directors must be instantiable. */ - init() +public protocol WillBeginTransition { /** Invoked on initiation of a view controller transition. @@ -33,6 +30,39 @@ public protocol Transition { func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] } +/** + A transition is responsible for describing the motion that will occur during a UIViewController + transition. + */ +public protocol Transition: WillBeginTransition { + + /** + Transitions must be instantiable. + */ + init() +} + +/** + A transition with presentation is able to customize the overall presentation of the transition, + including adding temporary views and changing the destination frame of the presented view + controller. + */ +public protocol TransitionWithPresentation: Transition { + + /** + Queried before the Transition object is instantiated and only once, when the fore view controller + is initially presented. + + The returned object is cached for the lifetime of the fore view controller. + + The returned presentation controller may choose to conform to WillBeginTransition in order to + associate reactive motion with the transition. + */ + static func presentationController(forPresented presented: UIViewController, + presenting: UIViewController?, + source: UIViewController) -> UIPresentationController +} + /** A self-dismissing transition is given an opportunity to register gesture recognizers that will cause the presented view controller to be dismissed. diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index afd1d22..32f2ca2 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -92,6 +92,8 @@ public final class TransitionContext: NSObject { /** The runtime to which motion should be registered. */ fileprivate var runtime: MotionRuntime! + fileprivate let presentationController: UIPresentationController? + weak var delegate: TransitionDelegate? init(transitionType: Transition.Type, @@ -99,7 +101,8 @@ public final class TransitionContext: NSObject { back: UIViewController, fore: UIViewController, gestureRecognizers: Set, - foreAlignmentEdge: CGRectEdge?) { + foreAlignmentEdge: CGRectEdge?, + presentationController: UIPresentationController?) { self.direction = createProperty("Transition.direction", withInitialValue: direction) self.initialDirection = direction self.back = back @@ -107,6 +110,7 @@ public final class TransitionContext: NSObject { self.gestureRecognizers = gestureRecognizers self.foreAlignmentEdge = foreAlignmentEdge self.window = TransitionTimeWindow(duration: TransitionContext.defaultDuration) + self.presentationController = presentationController // TODO: Create a Timeline. @@ -146,30 +150,6 @@ extension TransitionContext: UIViewControllerInteractiveTransitioning { } } -private func preferredFrame(for viewController: UIViewController, - inBounds bounds: CGRect, - alignmentEdge: CGRectEdge?) -> CGRect? { - guard viewController.preferredContentSize != .zero() else { - return nil - } - - let size = viewController.preferredContentSize - let origin: CGPoint - switch alignmentEdge { - case nil: // Centered - origin = .init(x: bounds.midX - size.width / 2, y: bounds.midY - size.height / 2) - case .minXEdge?: - origin = .init(x: bounds.minX, y: bounds.midY - size.height / 2) - case .minYEdge?: - origin = .init(x: bounds.midX - size.width / 2, y: bounds.minY) - case .maxXEdge?: - origin = .init(x: bounds.maxX - size.width, y: bounds.midY - size.height / 2) - case .maxYEdge?: - origin = .init(x: bounds.midX - size.width / 2, y: bounds.maxY - size.height) - } - return CGRect(origin: origin, size: size) -} - extension TransitionContext { fileprivate func initiateTransition() { if let from = context.viewController(forKey: .from) { @@ -180,15 +160,7 @@ extension TransitionContext { } if let to = context.viewController(forKey: .to) { - let finalFrame: CGRect - - if let preferredFrame = preferredFrame(for: to, - inBounds: context.containerView.bounds, - alignmentEdge: (direction.value == .forward) ? foreAlignmentEdge : nil) { - finalFrame = preferredFrame - } else { - finalFrame = context.finalFrame(for: to) - } + let finalFrame = context.finalFrame(for: to) if !finalFrame.isEmpty { to.view.frame = finalFrame } @@ -210,7 +182,13 @@ extension TransitionContext { pokeSystemAnimations() - let terminators = transition.willBeginTransition(withContext: self, runtime: self.runtime) + var terminators = transition.willBeginTransition(withContext: self, runtime: self.runtime) + + if let presentationController = presentationController as? WillBeginTransition { + terminators.append(contentsOf: presentationController.willBeginTransition(withContext: self, + runtime: self.runtime)) + } + runtime.whenAllAtRest(terminators) { [weak self] in self?.terminate() } diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index d583b58..1170db7 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -68,6 +68,32 @@ public final class TransitionController { get { return _transitioningDelegate.transitionType } } + /** + The presentation controller to be used during this transition. + + Will be read from and cached when the view controller is first presented. Changes made to this + property after presentation will be ignored. + */ + public var presentationController: UIPresentationController? { + set { _transitioningDelegate.presentationController = newValue } + get { return _transitioningDelegate.presentationController } + } + + /** + The edge to align the fore view's frame to. + + Defaults to nil, which will center the fore view's frame in the container's bounds. + + Use this property in conjunction with fore's preferredContentSize to create dialogs that fill + part of the screen. + + This property will only be read if the fore view controller's modalPresentationStyle is .custom. + */ + public var foreAlignmentEdge: CGRectEdge? { + set { _transitioningDelegate.foreAlignmentEdge = newValue } + get { return _transitioningDelegate.foreAlignmentEdge } + } + /** Start a dismiss transition when the given gesture recognizer enters its began or recognized state. @@ -109,11 +135,6 @@ public final class TransitionController { get { return _transitioningDelegate.gestureDelegate.gestureRecognizers } } - public var foreAlignmentEdge: CGRectEdge? { - set { _transitioningDelegate.foreAlignmentEdge = newValue } - get { return _transitioningDelegate.foreAlignmentEdge } - } - /** The transitioning delegate managed by this controller. @@ -163,6 +184,7 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni let dismisser: ViewControllerDismisser let gestureDelegate = GestureDelegate() + var presentationController: UIPresentationController? weak var associatedViewController: UIViewController? @@ -187,7 +209,8 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni back: back, fore: fore, gestureRecognizers: gestureDelegate.gestureRecognizers, - foreAlignmentEdge: foreAlignmentEdge) + foreAlignmentEdge: foreAlignmentEdge, + presentationController: presentationController) ctx?.delegate = self } } @@ -195,10 +218,7 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { - prepareForTransition(withSource: source, - back: presenting, - fore: presented, - direction: .forward) + prepareForTransition(withSource: source, back: presenting, fore: presented, direction: .forward) return ctx } @@ -240,6 +260,19 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni func isInteractive() -> Bool { return gestureDelegate.gestureRecognizers.filter { $0.state == .began || $0.state == .changed }.count > 0 } + + func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + guard let transitionWithPresentation = transitionType as? TransitionWithPresentation.Type else { + return nil + } + if let presentationController = presentationController { + return presentationController + } + presentationController = transitionWithPresentation.presentationController(forPresented: presented, + presenting: presenting, + source: source) + return presentationController + } } extension TransitioningDelegate: ViewControllerDismisserDelegate { From 235a3e7f3a4b78678b6e841c6e196471436555f4 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 10 May 2017 13:36:26 -0400 Subject: [PATCH 32/60] Remove the foreAlignmentEdge property from the transition controller. Summary: This will be moved into the presentation controller instead. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3198 --- src/transitions/TransitionContext.swift | 6 ------ src/transitions/TransitionController.swift | 17 ----------------- 2 files changed, 23 deletions(-) diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index 32f2ca2..06371b1 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -84,8 +84,6 @@ public final class TransitionContext: NSObject { */ public let fore: UIViewController - public let foreAlignmentEdge: CGRectEdge? - /** The set of gesture recognizers associated with this transition. */ public let gestureRecognizers: Set @@ -101,19 +99,15 @@ public final class TransitionContext: NSObject { back: UIViewController, fore: UIViewController, gestureRecognizers: Set, - foreAlignmentEdge: CGRectEdge?, presentationController: UIPresentationController?) { self.direction = createProperty("Transition.direction", withInitialValue: direction) self.initialDirection = direction self.back = back self.fore = fore self.gestureRecognizers = gestureRecognizers - self.foreAlignmentEdge = foreAlignmentEdge self.window = TransitionTimeWindow(duration: TransitionContext.defaultDuration) self.presentationController = presentationController - // TODO: Create a Timeline. - self.transition = transitionType.init() super.init() diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index 1170db7..52ef230 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -79,21 +79,6 @@ public final class TransitionController { get { return _transitioningDelegate.presentationController } } - /** - The edge to align the fore view's frame to. - - Defaults to nil, which will center the fore view's frame in the container's bounds. - - Use this property in conjunction with fore's preferredContentSize to create dialogs that fill - part of the screen. - - This property will only be read if the fore view controller's modalPresentationStyle is .custom. - */ - public var foreAlignmentEdge: CGRectEdge? { - set { _transitioningDelegate.foreAlignmentEdge = newValue } - get { return _transitioningDelegate.foreAlignmentEdge } - } - /** Start a dismiss transition when the given gesture recognizer enters its began or recognized state. @@ -178,7 +163,6 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni self.dismisser.delegate = self } - var foreAlignmentEdge: CGRectEdge? var ctx: TransitionContext? var transitionType: Transition.Type? @@ -209,7 +193,6 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni back: back, fore: fore, gestureRecognizers: gestureDelegate.gestureRecognizers, - foreAlignmentEdge: foreAlignmentEdge, presentationController: presentationController) ctx?.delegate = self } From 5b6149d5646e3fab1188e8e2c1714d02dc5d0ce8 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 10 May 2017 17:07:03 -0400 Subject: [PATCH 33/60] Allow transition types to be instantiated and stored on the transition controller. Summary: This makes it possible to configure a transition's behavior. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator Reviewed By: randcode-generator Tags: #material_motion Differential Revision: http://codereview.cc/D3199 --- examples/ContextualTransitionExample.swift | 4 +--- examples/FabTransitionExample.swift | 9 ++++---- ...InteractivePushBackTransitionExample.swift | 4 +--- examples/ModalDialogExample.swift | 6 ++--- examples/PushBackTransitionExample.swift | 4 +--- examples/StickerPickerExample.swift | 2 +- src/transitions/Transition.swift | 15 ++++++------- src/transitions/TransitionContext.swift | 10 ++++++--- src/transitions/TransitionController.swift | 22 +++++++++---------- 9 files changed, 35 insertions(+), 41 deletions(-) diff --git a/examples/ContextualTransitionExample.swift b/examples/ContextualTransitionExample.swift index db4ba3e..f4a05dc 100644 --- a/examples/ContextualTransitionExample.swift +++ b/examples/ContextualTransitionExample.swift @@ -165,7 +165,7 @@ class PhotoAlbumViewController: UIViewController, UICollectionViewDataSource, UI super.init(nibName: nil, bundle: nil) - transitionController.transitionType = PushBackTransition.self + transitionController.transition = PushBackTransition() } required init?(coder aDecoder: NSCoder) { @@ -258,8 +258,6 @@ class PhotoAlbumViewController: UIViewController, UICollectionViewDataSource, UI private class PushBackTransition: Transition { - required init() {} - func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] { let foreVC = ctx.fore as! PhotoAlbumViewController let foreImageView = (foreVC.collectionView.cellForItem(at: foreVC.indexPathForCurrentPhoto()) as! PhotoCollectionViewCell).imageView diff --git a/examples/FabTransitionExample.swift b/examples/FabTransitionExample.swift index fd6189c..03660c8 100644 --- a/examples/FabTransitionExample.swift +++ b/examples/FabTransitionExample.swift @@ -41,7 +41,7 @@ class FabTransitionExampleViewController: ExampleViewController, TransitionConte func didTap() { let vc = ModalViewController() - vc.transitionController.transitionType = CircularRevealTransition.self + vc.transitionController.transition = CircularRevealTransition() present(vc, animated: true) } @@ -76,18 +76,17 @@ private class ModalViewController: UIViewController { let floodFillOvershootRatio: CGFloat = 1.2 -private class CircularRevealTransition: Transition { +private class CircularRevealTransition: TransitionWithTermination { // TODO: Support for transient views. var floodFillView: UIView! var foreViewLayer: CALayer! - deinit { + + func didEndTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) { floodFillView.removeFromSuperview() foreViewLayer.mask = nil } - required init() {} - func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] { foreViewLayer = ctx.fore.view.layer diff --git a/examples/InteractivePushBackTransitionExample.swift b/examples/InteractivePushBackTransitionExample.swift index bb9e718..a47e072 100644 --- a/examples/InteractivePushBackTransitionExample.swift +++ b/examples/InteractivePushBackTransitionExample.swift @@ -41,7 +41,7 @@ private class ModalViewController: UIViewController, UIGestureRecognizerDelegate override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - transitionController.transitionType = PushBackTransition.self + transitionController.transition = PushBackTransition() } required init?(coder aDecoder: NSCoder) { @@ -72,8 +72,6 @@ private class ModalViewController: UIViewController, UIGestureRecognizerDelegate private class PushBackTransition: Transition { - required init() {} - func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] { let draggable = Draggable(withFirstGestureIn: ctx.gestureRecognizers) diff --git a/examples/ModalDialogExample.swift b/examples/ModalDialogExample.swift index 9756f04..a027bb3 100644 --- a/examples/ModalDialogExample.swift +++ b/examples/ModalDialogExample.swift @@ -45,7 +45,7 @@ class ModalDialogViewController: UIViewController { override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - transitionController.transitionType = ModalDialogTransition.self + transitionController.transition = ModalDialogTransition() preferredContentSize = .init(width: 200, height: 200) modalPresentationStyle = .overCurrentContext } @@ -69,8 +69,6 @@ class ModalDialogViewController: UIViewController { class ModalDialogTransition: SelfDismissingTransition { - required init() {} - func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] { let size = ctx.fore.view.frame.size let bounds = ctx.containerView().bounds @@ -106,7 +104,7 @@ class ModalDialogTransition: SelfDismissingTransition { return [tossable.spring] } - static func willPresent(fore: UIViewController, dismisser: ViewControllerDismisser) { + func willPresent(fore: UIViewController, dismisser: ViewControllerDismisser) { let tap = UITapGestureRecognizer() fore.view.addGestureRecognizer(tap) dismisser.dismissWhenGestureRecognizerBegins(tap) diff --git a/examples/PushBackTransitionExample.swift b/examples/PushBackTransitionExample.swift index 60da235..8cf2571 100644 --- a/examples/PushBackTransitionExample.swift +++ b/examples/PushBackTransitionExample.swift @@ -41,7 +41,7 @@ private class ModalViewController: UIViewController { override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - transitionController.transitionType = PushBackTransition.self + transitionController.transition = PushBackTransition() } required init?(coder aDecoder: NSCoder) { @@ -67,8 +67,6 @@ private class ModalViewController: UIViewController { private class PushBackTransition: Transition { - required init() {} - func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] { let bounds = ctx.containerView().bounds let backPosition = bounds.maxY + ctx.fore.view.bounds.height / 2 diff --git a/examples/StickerPickerExample.swift b/examples/StickerPickerExample.swift index 895f679..31a83b4 100644 --- a/examples/StickerPickerExample.swift +++ b/examples/StickerPickerExample.swift @@ -108,7 +108,7 @@ private class StickerListViewController: UICollectionViewController { init() { super.init(collectionViewLayout: UICollectionViewFlowLayout()) - transitionController.transitionType = ModalTransition.self + transitionController.transition = ModalTransition() modalPresentationStyle = .overCurrentContext } diff --git a/src/transitions/Transition.swift b/src/transitions/Transition.swift index c61f43f..c37e229 100644 --- a/src/transitions/Transition.swift +++ b/src/transitions/Transition.swift @@ -18,10 +18,10 @@ import Foundation import UIKit /** - An object that is capable of responding to a transition that is about to begin. + A transition is responsible for describing the motion that will occur during a UIViewController + transition. */ -public protocol WillBeginTransition { - +public protocol Transition { /** Invoked on initiation of a view controller transition. @@ -34,12 +34,11 @@ public protocol WillBeginTransition { A transition is responsible for describing the motion that will occur during a UIViewController transition. */ -public protocol Transition: WillBeginTransition { - +public protocol TransitionWithTermination: Transition { /** - Transitions must be instantiable. + Invoked on completion of a view controller transition. */ - init() + func didEndTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) } /** @@ -68,5 +67,5 @@ public protocol TransitionWithPresentation: Transition { cause the presented view controller to be dismissed. */ public protocol SelfDismissingTransition: Transition { - static func willPresent(fore: UIViewController, dismisser: ViewControllerDismisser) + func willPresent(fore: UIViewController, dismisser: ViewControllerDismisser) } diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index 06371b1..5ac3ca7 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -94,7 +94,7 @@ public final class TransitionContext: NSObject { weak var delegate: TransitionDelegate? - init(transitionType: Transition.Type, + init(transition: Transition, direction: TransitionDirection, back: UIViewController, fore: UIViewController, @@ -108,7 +108,7 @@ public final class TransitionContext: NSObject { self.window = TransitionTimeWindow(duration: TransitionContext.defaultDuration) self.presentationController = presentationController - self.transition = transitionType.init() + self.transition = transition super.init() } @@ -178,7 +178,7 @@ extension TransitionContext { var terminators = transition.willBeginTransition(withContext: self, runtime: self.runtime) - if let presentationController = presentationController as? WillBeginTransition { + if let presentationController = presentationController as? Transition { terminators.append(contentsOf: presentationController.willBeginTransition(withContext: self, runtime: self.runtime)) } @@ -251,6 +251,10 @@ extension TransitionContext { } context.completeTransition(completedInOriginalDirection) + if let transitionWithTermination = transition as? TransitionWithTermination { + transitionWithTermination.didEndTransition(withContext: self, runtime: runtime) + } + runtime = nil transition = nil diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index 52ef230..e575d74 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -63,9 +63,9 @@ public final class TransitionController { Must be a subclass of MDMTransition. */ - public var transitionType: Transition.Type? { - set { _transitioningDelegate.transitionType = newValue } - get { return _transitioningDelegate.transitionType } + public var transition: Transition? { + set { _transitioningDelegate.transition = newValue } + get { return _transitioningDelegate.transition } } /** @@ -164,7 +164,7 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni } var ctx: TransitionContext? - var transitionType: Transition.Type? + var transition: Transition? let dismisser: ViewControllerDismisser let gestureDelegate = GestureDelegate() @@ -183,12 +183,12 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni } assert(ctx == nil, "A transition is already active.") - if let transitionType = transitionType { - if direction == .forward, let selfDismissingDirector = transitionType as? SelfDismissingTransition.Type { + if let transition = transition { + if direction == .forward, let selfDismissingDirector = transition as? SelfDismissingTransition { selfDismissingDirector.willPresent(fore: fore, dismisser: dismisser) } - ctx = TransitionContext(transitionType: transitionType, + ctx = TransitionContext(transition: transition, direction: direction, back: back, fore: fore, @@ -245,15 +245,15 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni } func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { - guard let transitionWithPresentation = transitionType as? TransitionWithPresentation.Type else { + guard let transitionWithPresentation = transition as? TransitionWithPresentation else { return nil } if let presentationController = presentationController { return presentationController } - presentationController = transitionWithPresentation.presentationController(forPresented: presented, - presenting: presenting, - source: source) + presentationController = type(of: transitionWithPresentation).presentationController(forPresented: presented, + presenting: presenting, + source: source) return presentationController } } From b0be085ca0a5474e3c27a0ba1bf1029161e28471 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 10 May 2017 17:28:31 -0400 Subject: [PATCH 34/60] When using a transition with presentation, use the .custom modal presentation style. Summary: Otherwise the presentation controller will not be used. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3201 --- src/transitions/TransitionController.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index e575d74..cd41c1d 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -64,7 +64,13 @@ public final class TransitionController { Must be a subclass of MDMTransition. */ public var transition: Transition? { - set { _transitioningDelegate.transition = newValue } + set { + _transitioningDelegate.transition = newValue + + if let transition = newValue as? TransitionWithPresentation { + _transitioningDelegate.associatedViewController?.modalPresentationStyle = .custom + } + } get { return _transitioningDelegate.transition } } From 5bef83680ad50d7b683f7f61e923274e6996ef08 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 11 May 2017 14:25:05 -0400 Subject: [PATCH 35/60] Add reactive UILabel type with text property. Summary: Closes https://github.com/material-motion/material-motion-swift/issues/84 Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3209 --- src/reactivetypes/Reactive+UILabel.swift | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/reactivetypes/Reactive+UILabel.swift diff --git a/src/reactivetypes/Reactive+UILabel.swift b/src/reactivetypes/Reactive+UILabel.swift new file mode 100644 index 0000000..080298b --- /dev/null +++ b/src/reactivetypes/Reactive+UILabel.swift @@ -0,0 +1,29 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +extension Reactive where O: UILabel { + + public var text: ReactiveProperty { + let view = _object + return _properties.named(#function) { + return .init("\(pretty(view)).\(#function)", initialValue: view.text ?? "") { + view.text = $0 + } + } + } +} From 0bcd0f14adcfd4d0f5aa92bfe56647e00372cf2d Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 11 May 2017 14:29:38 -0400 Subject: [PATCH 36/60] Add runtime.get for UISlider instances. Summary: Closes https://github.com/material-motion/material-motion-swift/issues/83 Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator Reviewed By: randcode-generator Tags: #material_motion Differential Revision: http://codereview.cc/D3210 --- src/MotionRuntime.swift | 7 ++++ src/systems/sliderToStream.swift | 60 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/systems/sliderToStream.swift diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift index 305037b..e7746f5 100644 --- a/src/MotionRuntime.swift +++ b/src/MotionRuntime.swift @@ -205,6 +205,13 @@ public final class MotionRuntime { return get(scrollView) { scrollViewToStream($0) } } + /** + Returns a reactive version of the given object and caches the returned result for future access. + */ + public func get(_ slider: UISlider) -> MotionObservable { + return get(slider) { sliderToStream($0) } + } + /** Returns a reactive version of the given object and caches the returned result for future access. */ diff --git a/src/systems/sliderToStream.swift b/src/systems/sliderToStream.swift new file mode 100644 index 0000000..6a46cd2 --- /dev/null +++ b/src/systems/sliderToStream.swift @@ -0,0 +1,60 @@ +/* + Copyright 2016-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit + +/** + Creates a scroll source backed by KVO on a UIScrollView. + + This scroll source will not emit state updates. + */ +func sliderToStream(_ slider: UISlider) -> MotionObservable { + return MotionObservable(Metadata("Slider", args: [slider])) { observer in + return SliderConnection(subscribedTo: slider, observer: observer).disconnect + } +} + +private final class SliderConnection: NSObject { + deinit { + disconnect() + } + + init(subscribedTo slider: UISlider, observer: MotionObserver) { + self.slider = slider + self.observer = observer + + super.init() + + slider.addTarget(self, action: #selector(sliderDidChange), for: .valueChanged) + + observer.next(CGFloat(slider.value)) + } + + func disconnect() { + slider?.removeTarget(self, action: #selector(sliderDidChange), for: .valueChanged) + slider = nil + } + + func sliderDidChange() { + if let slider = slider { + observer.next(CGFloat(slider.value)) + } + } + + private var slider: UISlider? + private let observer: MotionObserver +} From 6c9d84da86f4d5e614c6e1b9e9cc5baed685a1be Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 11 May 2017 14:59:33 -0400 Subject: [PATCH 37/60] Add format support to the toString operator for numerical types. Summary: This makes it possible to format numerical streams. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3211 --- src/operators/toString.swift | 12 ++++++++++++ src/reactivetypes/Reactive+UIButton.swift | 9 +++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/reactivetypes/Reactive+UIButton.swift diff --git a/src/operators/toString.swift b/src/operators/toString.swift index 410293d..aeb53f8 100644 --- a/src/operators/toString.swift +++ b/src/operators/toString.swift @@ -25,3 +25,15 @@ extension MotionObservableConvertible { return _map(#function) { String(describing: $0) } } } + +extension MotionObservableConvertible where T == CGFloat { + + /** + Emits a string representation of the incoming value. + + The incoming value may optionally be formatted according to the provided format string. + */ + public func toString(format: String) -> MotionObservable { + return _map(#function) { String(format: format, $0) } + } +} diff --git a/src/reactivetypes/Reactive+UIButton.swift b/src/reactivetypes/Reactive+UIButton.swift new file mode 100644 index 0000000..363a998 --- /dev/null +++ b/src/reactivetypes/Reactive+UIButton.swift @@ -0,0 +1,9 @@ +// +// Reactive+UIButton.swift +// Pods +// +// Created by Jeff Verkoeyen on 5/11/17. +// +// + +import Foundation From af75058c6896195a689873eab06613a2adec2798 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 11 May 2017 15:22:01 -0400 Subject: [PATCH 38/60] Add a reactive button target type and an initial isHighlighted stream. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3212 --- src/reactivetypes/Reactive+UIButton.swift | 63 ++++++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/src/reactivetypes/Reactive+UIButton.swift b/src/reactivetypes/Reactive+UIButton.swift index 363a998..835c341 100644 --- a/src/reactivetypes/Reactive+UIButton.swift +++ b/src/reactivetypes/Reactive+UIButton.swift @@ -1,9 +1,58 @@ -// -// Reactive+UIButton.swift -// Pods -// -// Created by Jeff Verkoeyen on 5/11/17. -// -// +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ import Foundation +import UIKit + +/** + A reactive button target exposes streams for certain button events. + */ +public final class ReactiveButtonTarget: NSObject { + public init(_ button: UIButton) { + super.init() + + button.addTarget(self, action: #selector(didEnterEvent), + for: [.touchDown, .touchDragInside]) + button.addTarget(self, action: #selector(didExitEvent), + for: [.touchUpInside, .touchUpOutside, .touchDragOutside]) + } + + // MARK: Streams + + /** + Emits true when the button should be highlighted and false when it should not. + */ + public var didHighlight: MotionObservable { + return MotionObservable { observer in + self.didHighlightObservers.append(observer) + return { + if let index = self.didHighlightObservers.index(where: { $0 === observer }) { + self.didHighlightObservers.remove(at: index) + } + } + } + } + + func didEnterEvent(_ button: UIButton) { + didHighlightObservers.forEach { $0.next(true) } + } + func didExitEvent(_ button: UIButton) { + didHighlightObservers.forEach { $0.next(false) } + } + private var didHighlightObservers: [MotionObserver] = [] + + public let metadata = Metadata("Button target") +} From 8d73d04ff707851e639e11c48f88ec1a363a66e3 Mon Sep 17 00:00:00 2001 From: Eric Tang Date: Fri, 12 May 2017 15:44:56 -0400 Subject: [PATCH 39/60] Rename keyPositions to offsets Summary: Fixed https://github.com/material-motion/material-motion-swift/issues/118 Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, featherless Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, featherless Tags: #material_motion Differential Revision: http://codereview.cc/D3221 --- examples/MaterialExpansionExample.swift | 2 +- src/interactions/TransitionTween.swift | 6 +++--- src/interactions/Tween.swift | 6 +++--- src/systems/coreAnimationTweenToStream.swift | 14 +++++++------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/MaterialExpansionExample.swift b/examples/MaterialExpansionExample.swift index f76b738..7948146 100644 --- a/examples/MaterialExpansionExample.swift +++ b/examples/MaterialExpansionExample.swift @@ -61,7 +61,7 @@ class MaterialExpansionExampleViewController: ExampleViewController { let floodExpansion = Tween(duration: 0.375, values: [0, 1]) floodExpansion.timingFunctions.value = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)] let fadeOut = Tween(duration: 0.375, values: [0.75, 0]) - fadeOut.keyPositions.value = [0.2, 1] + fadeOut.offsets.value = [0.2, 1] runtime.add(SetPositionOnTap(.withExistingRecognizer(tap.gestureRecognizer)), to: runtime.get(flood.layer).position) diff --git a/src/interactions/TransitionTween.swift b/src/interactions/TransitionTween.swift index 6b4048d..e6a26e8 100644 --- a/src/interactions/TransitionTween.swift +++ b/src/interactions/TransitionTween.swift @@ -69,7 +69,7 @@ public final class TransitionTween: Tween { self.direction = direction self.toggledValues = direction.dedupe().rewrite([.backward: backwardValues, .forward: forwardValues]) - self.toggledKeyPositions = direction.dedupe().rewrite([.backward: backwardKeyPositions, .forward: forwardKeyPositions]) + self.toggledOffsets = direction.dedupe().rewrite([.backward: backwardKeyPositions, .forward: forwardKeyPositions]) super.init(duration: duration, values: values, system: system, timeline: timeline) } @@ -94,7 +94,7 @@ public final class TransitionTween: Tween { let unlocked = createProperty("TransitionTween.unlocked", withInitialValue: false) runtime.connect(direction.dedupe().rewriteTo(false), to: unlocked) runtime.connect(toggledValues, to: values) - runtime.connect(toggledKeyPositions, to: keyPositions) + runtime.connect(toggledOffsets, to: offsets) super.add(to: property, withRuntime: runtime) { var stream = $0 if let constraints = constraints { @@ -107,5 +107,5 @@ public final class TransitionTween: Tween { private let direction: ReactiveProperty private let toggledValues: MotionObservable<[T]> - private let toggledKeyPositions: MotionObservable<[CGFloat]> + private let toggledOffsets: MotionObservable<[CGFloat]> } diff --git a/src/interactions/Tween.swift b/src/interactions/Tween.swift index 7c843c8..9faf333 100644 --- a/src/interactions/Tween.swift +++ b/src/interactions/Tween.swift @@ -82,7 +82,7 @@ public class Tween: Interaction, Togglable, Stateful { See CAKeyframeAnimation documentation for more details. */ - public let keyPositions = createProperty("Tween.keyPositions", withInitialValue: [] as [CGFloat]) + public let offsets = createProperty("Tween.offsets", withInitialValue: [] as [CGFloat]) /** An array of CAMediaTimingFunction objects. If the `values' array defines n keyframes, @@ -168,7 +168,7 @@ public struct TweenShadow { public let repeatDuration: ReactiveProperty public let autoreverses: ReactiveProperty public let values: ReactiveProperty<[T]> - public let keyPositions: ReactiveProperty<[CGFloat]> + public let offsets: ReactiveProperty<[CGFloat]> public let timingFunctions: ReactiveProperty<[CAMediaTimingFunction]> public let timeline: Timeline? @@ -181,7 +181,7 @@ public struct TweenShadow { self.autoreverses = tween.autoreverses self.delay = tween.delay self.values = tween.values - self.keyPositions = tween.keyPositions + self.offsets = tween.offsets self.timingFunctions = tween.timingFunctions self.timeline = tween.timeline } diff --git a/src/systems/coreAnimationTweenToStream.swift b/src/systems/coreAnimationTweenToStream.swift index ce52240..f63b6f9 100644 --- a/src/systems/coreAnimationTweenToStream.swift +++ b/src/systems/coreAnimationTweenToStream.swift @@ -40,10 +40,10 @@ private func streamFromTween(_ tween: TweenShadow, configureEvent: @escapi var animationKeys: [String] = [] var activeAnimations = Set() var lastValues: [T]? - var lastKeyPositions: [CGFloat]? + var lastOffsets: [CGFloat]? let checkAndEmit = { - guard let values = lastValues, let keyPositions = lastKeyPositions, tween.enabled.value else { + guard let values = lastValues, let offsets = lastOffsets, tween.enabled.value else { return } let animation: CAPropertyAnimation @@ -51,7 +51,7 @@ private func streamFromTween(_ tween: TweenShadow, configureEvent: @escapi if values.count > 1 { let keyframeAnimation = CAKeyframeAnimation() keyframeAnimation.values = values - keyframeAnimation.keyTimes = keyPositions.map { NSNumber(value: Double($0)) } + keyframeAnimation.keyTimes = offsets.map { NSNumber(value: Double($0)) } keyframeAnimation.timingFunctions = timingFunctions.value animation = keyframeAnimation } else { @@ -108,8 +108,8 @@ private func streamFromTween(_ tween: TweenShadow, configureEvent: @escapi checkAndEmit() } - let keyPositionsSubscription = tween.keyPositions.subscribeToValue { keyPositions in - lastKeyPositions = keyPositions + let offsetsSubscription = tween.offsets.subscribeToValue { offsets in + lastOffsets = offsets checkAndEmit() } @@ -119,10 +119,10 @@ private func streamFromTween(_ tween: TweenShadow, configureEvent: @escapi activeAnimations.removeAll() lastValues = nil - lastKeyPositions = nil + lastOffsets = nil activeSubscription.unsubscribe() valuesSubscription.unsubscribe() - keyPositionsSubscription.unsubscribe() + offsetsSubscription.unsubscribe() } } } From fddbc7e888f41e4ac0e10803c365d8116d245b57 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 12 May 2017 12:30:35 -0400 Subject: [PATCH 40/60] Remove Metadata. Summary: Visualization of the runtime is being deprioritized in favor of more focused tooling such as stream value visualization and reactive common controls. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator Reviewed By: randcode-generator Tags: #material_motion Differential Revision: http://codereview.cc/D3215 --- .../HowToMakeACustomOperatorExample.swift | 4 +- .../Contents.swift | 22 --- src/MotionRuntime.swift | 19 -- src/ReactiveProperty.swift | 35 ++-- src/debugging/Metadata.swift | 168 ------------------ src/interactions/ArcMove.swift | 8 +- src/interactions/PathTween.swift | 16 +- src/interactions/Spring.swift | 18 +- src/interactions/TransitionTween.swift | 2 +- src/interactions/Tween.swift | 20 +-- src/operators/anchorPointAdjustment.swift | 2 +- src/operators/dedupe.swift | 2 +- src/operators/delayBy.swift | 2 +- src/operators/distanceFrom.swift | 6 +- src/operators/foundation/_filter.swift | 4 +- src/operators/foundation/_map.swift | 4 +- src/operators/foundation/_nextOperator.swift | 8 +- src/operators/foundation/_remember.swift | 2 +- src/operators/gestures/centroid.swift | 2 +- src/operators/gestures/rotated.swift | 2 +- src/operators/gestures/scaled.swift | 2 +- src/operators/gestures/translation.swift | 2 +- .../gestures/translationAddedTo.swift | 2 +- src/operators/gestures/velocity.swift | 6 +- .../gestures/whenRecognitionStateIs.swift | 10 +- src/operators/inverted.swift | 4 +- src/operators/log.swift | 2 +- src/operators/lowerBound.swift | 2 +- src/operators/merge.swift | 2 +- src/operators/normalizedBy.swift | 4 +- src/operators/offsetBy.swift | 2 +- src/operators/rewrite.swift | 4 +- src/operators/rewriteRange.swift | 2 +- src/operators/rewriteTo.swift | 4 +- src/operators/rubberBanded.swift | 6 +- src/operators/scaledBy.swift | 4 +- src/operators/slop.swift | 2 +- src/operators/startWith.swift | 4 +- src/operators/threshold.swift | 2 +- src/operators/thresholdRange.swift | 2 +- src/operators/toString.swift | 4 +- src/operators/upperBound.swift | 2 +- src/operators/valve.swift | 2 +- src/operators/visualize.swift | 2 +- src/operators/x.swift | 4 +- src/operators/xLockedTo.swift | 4 +- src/operators/y.swift | 4 +- src/operators/yLockedTo.swift | 4 +- src/protocols/Stateful.swift | 4 +- src/reactivetypes/MotionObservable.swift | 24 +-- src/reactivetypes/Reactive+CALayer.swift | 90 ++++------ src/reactivetypes/Reactive+CAShapeLayer.swift | 7 +- src/reactivetypes/Reactive+UIButton.swift | 2 - src/reactivetypes/Reactive+UILabel.swift | 2 +- src/reactivetypes/Reactive+UIView.swift | 6 +- .../ReactiveScrollViewDelegate.swift | 2 - .../ReactiveUIGestureRecognizer.swift | 5 +- .../coreAnimationPathTweenToStream.swift | 2 +- src/systems/coreAnimationSpringToStream.swift | 2 +- src/systems/coreAnimationTweenToStream.swift | 2 +- src/systems/gestureToStream.swift | 2 +- src/systems/scrollViewToStream.swift | 2 +- src/systems/sliderToStream.swift | 2 +- src/timeline/Timeline.swift | 8 +- src/transitions/TransitionContext.swift | 2 +- tests/unit/MotionRuntimeTests.swift | 4 +- tests/unit/ReactivePropertyTests.swift | 6 +- 67 files changed, 168 insertions(+), 446 deletions(-) delete mode 100644 examples/apps/Catalog/ReactivePlayground.playground/Pages/Visualizing the runtime.xcplaygroundpage/Contents.swift delete mode 100644 src/debugging/Metadata.swift diff --git a/examples/HowToMakeACustomOperatorExample.swift b/examples/HowToMakeACustomOperatorExample.swift index 3e93ea1..d967834 100644 --- a/examples/HowToMakeACustomOperatorExample.swift +++ b/examples/HowToMakeACustomOperatorExample.swift @@ -40,8 +40,6 @@ class HowToMakeACustomOperatorExampleViewController: ExampleViewController { extension MotionObservableConvertible where T == CGPoint { fileprivate func wobble(width: CGFloat) -> MotionObservable { - return _map(#function) { - .init(x: $0.x + sin($0.y / 50) * width, y: $0.y) - } + return _map { .init(x: $0.x + sin($0.y / 50) * width, y: $0.y) } } } diff --git a/examples/apps/Catalog/ReactivePlayground.playground/Pages/Visualizing the runtime.xcplaygroundpage/Contents.swift b/examples/apps/Catalog/ReactivePlayground.playground/Pages/Visualizing the runtime.xcplaygroundpage/Contents.swift deleted file mode 100644 index 68234b1..0000000 --- a/examples/apps/Catalog/ReactivePlayground.playground/Pages/Visualizing the runtime.xcplaygroundpage/Contents.swift +++ /dev/null @@ -1,22 +0,0 @@ -/*: - ## Visualizing the runtime - - The motion runtime represents all of its interactions as **connected streams**, making it possible to visualize the internal state of the runtime as a directed graph. Use `runtime.asGraphviz()` to get a graphviz-compatible string for visualizing the runtime. - - In this page we'll use webgraphviz.com to visualize the runtime in our playground in real time. - */ -import MaterialMotion - -let view = createExampleView() -canvas.addSubview(view) -let runtime = MotionRuntime(containerView: canvas) - -//: --- -//: -//: Try adding new interactions and constraints below. - -runtime.add(Draggable(), to: view) { $0.xLocked(to: 100) } - -//: --- - -visualize(graphviz: runtime.asGraphviz(), onCanvas: canvas) diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift index e7746f5..35b9fe4 100644 --- a/src/MotionRuntime.swift +++ b/src/MotionRuntime.swift @@ -253,23 +253,6 @@ public final class MotionRuntime { self.subscriptions.append(contentsOf: subscriptions) } - /** - Generates a graphviz-compatiable representation of all interactions associated with the runtime. - - For quick previewing, use an online graphviz visualization tool like http://www.webgraphviz.com/ - */ - public func asGraphviz() -> String { - var lines: [String] = [ - "digraph G {", - "node [shape=rect]" - ] - for metadata in metadata { - lines.append(metadata.debugDescription) - } - lines.append("}") - return lines.joined(separator: "\n") - } - /** A Boolean stream indicating whether the runtime is currently being directly manipulated by the user. @@ -280,7 +263,6 @@ public final class MotionRuntime { private let aggregateManipulationState = AggregateMotionState() private func write(_ stream: O, to property: ReactiveProperty) where O.T == T { - metadata.append(stream.metadata.createChild(property.metadata)) subscriptions.append(stream.subscribe(next: { property.value = $0 }, coreAnimation: property.coreAnimation, visualization: { [weak self] view in @@ -307,7 +289,6 @@ public final class MotionRuntime { private var reactiveObjects: [ObjectIdentifier: AnyObject] = [:] private var targets: [ObjectIdentifier: [Any]] = [:] - private var metadata: [Metadata] = [] private var subscriptions: [Subscription] = [] private var interactions: [Any] = [] } diff --git a/src/ReactiveProperty.swift b/src/ReactiveProperty.swift index 214c0c6..85c5a4b 100644 --- a/src/ReactiveProperty.swift +++ b/src/ReactiveProperty.swift @@ -21,22 +21,22 @@ import IndefiniteObservable /** Creates a property with an initial value of zero. */ -public func createProperty(_ name: String? = nil) -> ReactiveProperty where T: Zeroable { - return ReactiveProperty(name, initialValue: T.zero() as! T) +public func createProperty() -> ReactiveProperty where T: Zeroable { + return ReactiveProperty(initialValue: T.zero() as! T) } /** Creates a CGFloat property with an initial value of zero. */ -public func createProperty(_ name: String? = nil) -> ReactiveProperty { - return ReactiveProperty(name, initialValue: 0) +public func createProperty() -> ReactiveProperty { + return ReactiveProperty(initialValue: 0) } /** Creates a property with a given initial value. */ -public func createProperty(_ name: String? = nil, withInitialValue initialValue: T) -> ReactiveProperty { - return ReactiveProperty(name, initialValue: initialValue) +public func createProperty(withInitialValue initialValue: T) -> ReactiveProperty { + return ReactiveProperty(initialValue: initialValue) } /** @@ -44,8 +44,8 @@ public func createProperty(_ name: String? = nil, withInitialValue initialVal If you need a ReactiveProperty instance, use ReactiveProperty's initializer instead. */ -public func createProperty(_ name: String? = nil, withInitialValue initialValue: Int) -> ReactiveProperty { - return ReactiveProperty(name, initialValue: CGFloat(initialValue)) +public func createProperty(withInitialValue initialValue: Int) -> ReactiveProperty { + return ReactiveProperty(initialValue: CGFloat(initialValue)) } /** @@ -71,8 +71,7 @@ public final class ReactiveProperty { /** Creates a new anonymous property. */ - public init(_ name: String? = nil, initialValue: T) { - self.metadata = Metadata(name, type: .property) + public init(initialValue: T) { self.value = initialValue self._externalWrite = nil self._coreAnimation = nil @@ -81,8 +80,7 @@ public final class ReactiveProperty { /** Creates a property that writes to some external information. */ - init(_ name: String? = nil, initialValue: T, externalWrite: @escaping NextChannel) { - self.metadata = Metadata(name, type: .property) + init(initialValue: T, externalWrite: @escaping NextChannel) { self.value = initialValue self._externalWrite = externalWrite self._coreAnimation = nil @@ -91,11 +89,7 @@ public final class ReactiveProperty { /** Creates a property that writes to some external information and supports Core Animation. */ - init(_ name: String? = nil, - initialValue: T, - externalWrite: @escaping NextChannel, - coreAnimation: @escaping CoreAnimationChannel) { - self.metadata = Metadata(name, type: .property) + init(initialValue: T, externalWrite: @escaping NextChannel, coreAnimation: @escaping CoreAnimationChannel) { self.value = initialValue self._externalWrite = externalWrite self._coreAnimation = coreAnimation @@ -129,11 +123,6 @@ public final class ReactiveProperty { } } - /** - The metadata describing this property. - */ - public let metadata: Metadata - private let _externalWrite: NextChannel? private let _coreAnimation: CoreAnimationChannel? @@ -167,7 +156,7 @@ extension ReactiveProperty where T: Equatable { // Reactive properties can be used as streams. extension ReactiveProperty: MotionObservableConvertible { public func asStream() -> MotionObservable { - return MotionObservable(metadata) { observer in + return MotionObservable { observer in self.observers.append(observer) observer.next(self.value) diff --git a/src/debugging/Metadata.swift b/src/debugging/Metadata.swift deleted file mode 100644 index e79576d..0000000 --- a/src/debugging/Metadata.swift +++ /dev/null @@ -1,168 +0,0 @@ -/* - Copyright 2017-present The Material Motion Authors. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import Foundation -import UIKit - -public protocol Inspectable { - var metadata: Metadata { get } -} - -public final class Metadata: CustomDebugStringConvertible { - enum Metatype { - case node - case constraint - case property - case constant - } - var type: Metatype - let name: String - let args: [Any]? - let label: String - var parent: Metadata? - - init(_ name: String? = nil, type: Metatype = .node, args: [Any]? = nil, parent: Metadata? = nil) { - if let name = name { - self.name = "\(name)(\(NSUUID().uuidString))" - self.label = name - } else { - self.name = "" - self.label = "Unnamed property" - } - self.args = args - self.parent = parent - self.type = type - } - - init(_ metadata: Metadata, type: Metatype = .node, parent: Metadata) { - self.name = metadata.name - self.label = metadata.label - self.args = metadata.args - self.type = type - self.parent = parent - } - - func createChild(_ metadata: Metadata, type: Metatype = .node) -> Metadata { - return Metadata(metadata, type: type, parent: self) - } - - private var style: String { - switch type { - case .constraint: - return "style=filled, fillcolor=\"#FF80AB\"" - case .node: - return "style=filled, fillcolor=\"#FFFFFF\"" - case .property: - return "style=filled, fillcolor=\"#C51162\"" - case .constant: - return "style=filled, color=white fillcolor=\"#111111\"" - } - } - - private func prettyLabel() -> String { - var description: [String] = [] - if let args = args { - var components = label.components(separatedBy: ":") - for i in 0.. {\"\(iterator.name)\" [label=\"\(iterator.prettyLabel())\", \(iterator.style)]}") - - iterator = parent - - // If any arguments have metadata associated with them then we create a temporary association - // between the arg and this iterator and describe that graph. - if let args = iterator.args { - for arg in args { - if let inspectable = arg as? Inspectable { - let metadata = inspectable.metadata.createChild(iterator) - description.append(metadata.debugDescription) - } - } - } - } - - return description.joined(separator: "\n").replacingOccurrences(of: "MaterialMotion.", with: "") - } -} - -func pretty(_ object: Any) -> String { - switch object { - case is String: fallthrough - case is Int: fallthrough - case is Bool: fallthrough - case is Double: fallthrough - case is CGFloat: fallthrough - case is UIColor: fallthrough - case is UIGestureRecognizerState: fallthrough - case is Float: - return "\(object)" - - case let object as Array: - return object.map(pretty).joined(separator: ", ") - - case let object as NSObject: - return "\(type(of: object))::\(pretty(ObjectIdentifier(object)))" - - case let object as AnyObject: - return "\(object)::\(pretty(ObjectIdentifier(object)))" - - default: - return "\(object)" - } -} - -func pretty(_ objectIdentifer: ObjectIdentifier) -> String { - return objectIdentifer.debugDescription - .replacingOccurrences(of: "ObjectIdentifier(0x0000", with: "0x") - .replacingOccurrences(of: ")", with: "") -} - -extension UIGestureRecognizerState: CustomDebugStringConvertible { - public var debugDescription: String { - switch self { - case .began: return ".began" - case .changed: return ".changed" - case .cancelled: return ".cancelled" - case .ended: return ".ended" - case .possible: return ".possible" - case .failed: return ".failed" - } - } -} diff --git a/src/interactions/ArcMove.swift b/src/interactions/ArcMove.swift index c9d801e..a1d975a 100644 --- a/src/interactions/ArcMove.swift +++ b/src/interactions/ArcMove.swift @@ -30,12 +30,12 @@ public final class ArcMove: Interaction, Togglable, Stateful { /** The initial position of the arc move animation. */ - public let from = createProperty("ArcMove.from", withInitialValue: CGPoint.zero) + public let from = createProperty(withInitialValue: CGPoint.zero) /** The final position of the arc move animation. */ - public let to = createProperty("ArcMove.to", withInitialValue: CGPoint.zero) + public let to = createProperty(withInitialValue: CGPoint.zero) /** The tween interaction that will interpolate between the from and to values. @@ -61,8 +61,6 @@ public final class ArcMove: Interaction, Togglable, Stateful { public var state: MotionObservable { return tween.state } - - public let metadata = Metadata("ArcMove") } // Given two positional streams, returns a stream that emits an arc move path between the two @@ -70,7 +68,7 @@ public final class ArcMove: Interaction, Togglable, Stateful { private func arcMove (from: O1, to: O2) -> MotionObservable where O1.T == CGPoint, O2.T == CGPoint { - return MotionObservable(Metadata(#function, args: [from, to])) { observer in + return MotionObservable { observer in var latestFrom: CGPoint? var latestTo: CGPoint? diff --git a/src/interactions/PathTween.swift b/src/interactions/PathTween.swift index 521c147..dc6b2b1 100644 --- a/src/interactions/PathTween.swift +++ b/src/interactions/PathTween.swift @@ -30,7 +30,7 @@ public final class PathTween: Interaction, Togglable, Stateful { /** The delay of the animation in seconds. */ - public let delay = createProperty("PathTween.delay", withInitialValue: 0) + public let delay = createProperty(withInitialValue: 0) /** The path this animation will follow. @@ -49,7 +49,7 @@ public final class PathTween: Interaction, Togglable, Stateful { Enabling a previously disabled tween will restart the animation from the beginning. */ - public let enabled = createProperty("PathTween.enabled", withInitialValue: true) + public let enabled = createProperty(withInitialValue: true) /** The current state of the tween animation. @@ -62,8 +62,8 @@ public final class PathTween: Interaction, Togglable, Stateful { Initializes a path tween instance with its required properties. */ public init(duration: CGFloat, path: CGPath, system: @escaping PathTweenToStream = coreAnimation, timeline: Timeline? = nil) { - self.duration = createProperty("PathTween.duration", withInitialValue: duration) - self.path = createProperty("PathTween.path", withInitialValue: path) + self.duration = createProperty(withInitialValue: duration) + self.path = createProperty(withInitialValue: path) self.system = system self.timeline = timeline } @@ -83,8 +83,8 @@ public final class PathTween: Interaction, Togglable, Stateful { animation. */ public init(system: @escaping PathTweenToStream = coreAnimation, timeline: Timeline? = nil) { - self.duration = createProperty("PathTween.duration", withInitialValue: 0) - self.path = createProperty("PathTween.path", withInitialValue: UIBezierPath().cgPath) + self.duration = createProperty(withInitialValue: 0) + self.path = createProperty(withInitialValue: UIBezierPath().cgPath) self.system = system self.timeline = timeline } @@ -93,11 +93,9 @@ public final class PathTween: Interaction, Togglable, Stateful { runtime.connect(asStream(), to: property) } - public let metadata = Metadata("Path Tween") - fileprivate var stream: MotionObservable? fileprivate let system: PathTweenToStream - fileprivate let _state = createProperty("PathTween._state", withInitialValue: MotionState.atRest) + fileprivate let _state = createProperty(withInitialValue: MotionState.atRest) } public struct PathTweenShadow { diff --git a/src/interactions/Spring.swift b/src/interactions/Spring.swift index 92726ad..5d66100 100644 --- a/src/interactions/Spring.swift +++ b/src/interactions/Spring.swift @@ -53,7 +53,7 @@ public class Spring: Interaction, Togglable, Stateful where T: Zeroable, T: S - parameter system: The system that should be used to drive this spring. */ public init(threshold: CGFloat = 1, system: @escaping SpringToStream = coreAnimation) { - self.threshold = createProperty("Spring.threshold", withInitialValue: threshold) + self.threshold = createProperty(withInitialValue: threshold) self.system = system } @@ -62,35 +62,35 @@ public class Spring: Interaction, Togglable, Stateful where T: Zeroable, T: S Applied to the physical simulation only when it starts. */ - public let initialVelocity = createProperty("Spring.initialVelocity", withInitialValue: T.zero() as! T) + public let initialVelocity = createProperty(withInitialValue: T.zero() as! T) /** The destination value of the spring represented as a property. Changing this property will immediately affect the spring simulation. */ - public let destination = createProperty("Spring.destination", withInitialValue: T.zero() as! T) + public let destination = createProperty(withInitialValue: T.zero() as! T) /** Tension defines how quickly the spring's value moves towards its destination. Higher tension means higher initial velocity and more overshoot. */ - public let tension = createProperty("Spring.tension", withInitialValue: defaultSpringTension) + public let tension = createProperty(withInitialValue: defaultSpringTension) /** Tension defines how quickly the spring's velocity slows down. Higher friction means quicker deceleration and less overshoot. */ - public let friction = createProperty("Spring.friction", withInitialValue: defaultSpringFriction) + public let friction = createProperty(withInitialValue: defaultSpringFriction) /** The mass affects the value's acceleration. Higher mass means slower acceleration and deceleration. */ - public let mass = createProperty("Spring.mass", withInitialValue: defaultSpringMass) + public let mass = createProperty(withInitialValue: defaultSpringMass) /** The suggested duration of the spring represented as a property. @@ -99,7 +99,7 @@ public class Spring: Interaction, Togglable, Stateful where T: Zeroable, T: S A value of 0 means this property will be ignored. */ - public let suggestedDuration = createProperty("Spring.suggestedDuration", withInitialValue: 0) + public let suggestedDuration = createProperty(withInitialValue: 0) /** The value used when determining completion of the spring simulation. @@ -111,7 +111,7 @@ public class Spring: Interaction, Togglable, Stateful where T: Zeroable, T: S Enabling a previously disabled spring will restart the animation from the current initial value. */ - public let enabled = createProperty("Spring.enabled", withInitialValue: true) + public let enabled = createProperty(withInitialValue: true) /** The current state of the spring animation. @@ -132,8 +132,6 @@ public class Spring: Interaction, Togglable, Stateful where T: Zeroable, T: S runtime.connect(stream, to: property) } - public let metadata = Metadata("Spring") - fileprivate let system: SpringToStream private let aggregateState = AggregateMotionState() diff --git a/src/interactions/TransitionTween.swift b/src/interactions/TransitionTween.swift index e6a26e8..79b3ed3 100644 --- a/src/interactions/TransitionTween.swift +++ b/src/interactions/TransitionTween.swift @@ -91,7 +91,7 @@ public final class TransitionTween: Tween { public override func add(to property: ReactiveProperty, withRuntime runtime: MotionRuntime, constraints: ConstraintApplicator? = nil) { - let unlocked = createProperty("TransitionTween.unlocked", withInitialValue: false) + let unlocked = createProperty(withInitialValue: false) runtime.connect(direction.dedupe().rewriteTo(false), to: unlocked) runtime.connect(toggledValues, to: values) runtime.connect(toggledOffsets, to: offsets) diff --git a/src/interactions/Tween.swift b/src/interactions/Tween.swift index 9faf333..82c8910 100644 --- a/src/interactions/Tween.swift +++ b/src/interactions/Tween.swift @@ -41,7 +41,7 @@ public class Tween: Interaction, Togglable, Stateful { See https://developer.apple.com/reference/quartzcore/camediatiming/1427666-repeatcount for more information. */ - public let repeatCount: ReactiveProperty = createProperty("Tween.repeatCount", withInitialValue: 0) + public let repeatCount: ReactiveProperty = createProperty(withInitialValue: 0) /** The number of seconds the animation will repeat for. @@ -50,19 +50,19 @@ public class Tween: Interaction, Togglable, Stateful { See https://developer.apple.com/reference/quartzcore/camediatiming/1427643-repeatduration for more information. */ - public let repeatDuration: ReactiveProperty = createProperty("Tween.repeatDuration", withInitialValue: 0) + public let repeatDuration: ReactiveProperty = createProperty(withInitialValue: 0) /** Will the animation play in the reverse upon completion. See https://developer.apple.com/reference/quartzcore/camediatiming/1427645-autoreverses for more information. */ - public let autoreverses: ReactiveProperty = createProperty("Tween.autoreverses", withInitialValue: false) + public let autoreverses: ReactiveProperty = createProperty(withInitialValue: false) /** The delay of the animation in seconds. */ - public let delay = createProperty("Tween.delay", withInitialValue: 0) + public let delay = createProperty(withInitialValue: 0) /** An array of objects providing the value of the animation for each keyframe. @@ -82,7 +82,7 @@ public class Tween: Interaction, Togglable, Stateful { See CAKeyframeAnimation documentation for more details. */ - public let offsets = createProperty("Tween.offsets", withInitialValue: [] as [CGFloat]) + public let offsets = createProperty(withInitialValue: [] as [CGFloat]) /** An array of CAMediaTimingFunction objects. If the `values' array defines n keyframes, @@ -94,7 +94,7 @@ public class Tween: Interaction, Togglable, Stateful { See CAKeyframeAnimation documentation for more details. */ - public let timingFunctions = createProperty("Tween.timingFunctions", withInitialValue: + public let timingFunctions = createProperty(withInitialValue: [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)] ) @@ -110,7 +110,7 @@ public class Tween: Interaction, Togglable, Stateful { Enabling a previously disabled tween will restart the animation from the beginning. */ - public let enabled = createProperty("Tween.enabled", withInitialValue: true) + public let enabled = createProperty(withInitialValue: true) /** The current state of the tween animation. @@ -123,8 +123,8 @@ public class Tween: Interaction, Togglable, Stateful { Initializes a tween instance with its required properties. */ public init(duration: CGFloat, values: [T], system: @escaping TweenToStream = coreAnimation, timeline: Timeline? = nil) { - self.duration = createProperty("Tween.duration", withInitialValue: duration) - self.values = createProperty("Tween.values", withInitialValue: values) + self.duration = createProperty(withInitialValue: duration) + self.values = createProperty(withInitialValue: values) self.system = system self.timeline = timeline } @@ -156,7 +156,7 @@ public class Tween: Interaction, Togglable, Stateful { fileprivate let system: TweenToStream fileprivate var stream: MotionObservable? - fileprivate let _state = createProperty("Tween._state", withInitialValue: MotionState.atRest) + fileprivate let _state = createProperty(withInitialValue: MotionState.atRest) } public struct TweenShadow { diff --git a/src/operators/anchorPointAdjustment.swift b/src/operators/anchorPointAdjustment.swift index e450c9a..a009c7f 100644 --- a/src/operators/anchorPointAdjustment.swift +++ b/src/operators/anchorPointAdjustment.swift @@ -26,7 +26,7 @@ extension MotionObservableConvertible where T == CGPoint { top/left-most edge of the view's bounds and 1 means the right/bottom-most edge of the bounds. */ public func anchorPointAdjustment(in view: UIView) -> MotionObservable { - return _map(#function, args: [view]) { + return _map { let newPosition = CGPoint(x: $0.x * view.layer.bounds.width, y: $0.y * view.layer.bounds.height) let positionInSuperview = view.layer.convert(newPosition, to: view.layer.superlayer) return .init(anchorPoint: $0, position: positionInSuperview) diff --git a/src/operators/dedupe.swift b/src/operators/dedupe.swift index 9e1acda..6385266 100644 --- a/src/operators/dedupe.swift +++ b/src/operators/dedupe.swift @@ -24,7 +24,7 @@ extension MotionObservableConvertible where T: Equatable { public func dedupe() -> MotionObservable { var emitted = false var lastValue: T? - return _nextOperator(#function) { value, next in + return _nextOperator { value, next in if emitted && lastValue == value { return } diff --git a/src/operators/delayBy.swift b/src/operators/delayBy.swift index cff4afd..f20864f 100644 --- a/src/operators/delayBy.swift +++ b/src/operators/delayBy.swift @@ -24,7 +24,7 @@ extension MotionObservableConvertible { Emits values from upstream after the specified delay. */ public func delay(by duration: CGFloat) -> MotionObservable { - return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [duration]))) { observer in + return MotionObservable { observer in var subscription: Subscription? subscription = self.asStream().subscribeAndForward(to: observer) { value in diff --git a/src/operators/distanceFrom.swift b/src/operators/distanceFrom.swift index 3ed207b..4093e2f 100644 --- a/src/operators/distanceFrom.swift +++ b/src/operators/distanceFrom.swift @@ -23,7 +23,7 @@ extension MotionObservableConvertible where T == CGFloat { Emits the distance between the incoming value and the location. */ public func distance(from location: CGFloat) -> MotionObservable { - return _map(#function, args: [location]) { + return _map { fabs($0 - location) } } @@ -35,7 +35,7 @@ extension MotionObservableConvertible where T == CGPoint { Emits the distance between the incoming value and the location. */ public func distance(from location: CGPoint) -> MotionObservable { - return _map(#function, args: [location]) { + return _map { let xDelta = $0.x - location.x let yDelta = $0.y - location.y return sqrt(xDelta * xDelta + yDelta * yDelta) @@ -48,7 +48,7 @@ extension MotionObservableConvertible where T == CGPoint { public func distance(from location: O) -> MotionObservable where O.T == CGPoint { var lastLocation: CGPoint? var lastValue: CGPoint? - return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [location]))) { observer in + return MotionObservable { observer in let checkAndEmit = { guard let location = lastLocation, let value = lastValue else { diff --git a/src/operators/foundation/_filter.swift b/src/operators/foundation/_filter.swift index 7483ee4..c6ee2bf 100644 --- a/src/operators/foundation/_filter.swift +++ b/src/operators/foundation/_filter.swift @@ -23,8 +23,8 @@ extension MotionObservableConvertible { This operator is meant to be used when building other operators. */ - public func _filter(_ name: String? = nil, args: [Any]? = nil, predicate: @escaping (T) -> Bool) -> MotionObservable { - return _nextOperator(name, args: args) { value, next in + public func _filter(predicate: @escaping (T) -> Bool) -> MotionObservable { + return _nextOperator { value, next in if predicate(value) { next(value) } diff --git a/src/operators/foundation/_map.swift b/src/operators/foundation/_map.swift index 5ddaed2..9a4a73f 100644 --- a/src/operators/foundation/_map.swift +++ b/src/operators/foundation/_map.swift @@ -24,8 +24,8 @@ extension MotionObservableConvertible { This operator is meant to be used when building other operators. */ - public func _map(_ name: String? = nil, args: [Any]? = nil, transformVelocity: Bool = false, transform: @escaping (T) -> U) -> MotionObservable { - return _nextOperator(name, args: args, operation: { value, next in + public func _map(transformVelocity: Bool = false, transform: @escaping (T) -> U) -> MotionObservable { + return _nextOperator(operation: { 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 a6d99fc..e4d14bb 100644 --- a/src/operators/foundation/_nextOperator.swift +++ b/src/operators/foundation/_nextOperator.swift @@ -24,8 +24,8 @@ extension MotionObservableConvertible { This is the preferred method for building new operators. This builder can be used to create any operator that only needs to modify values. All state events are forwarded along. */ - func _nextOperator(_ name: String? = nil, args: [Any]? = nil, operation: @escaping (T, @escaping (U) -> Void) -> Void) -> MotionObservable { - return MotionObservable(self.metadata.createChild(Metadata(name, args: args), type: .constraint)) { observer in + func _nextOperator(operation: @escaping (T, @escaping (U) -> Void) -> Void) -> MotionObservable { + return MotionObservable { observer in return self.subscribe(next: { return operation($0, observer.next) }, coreAnimation: { _ in @@ -40,8 +40,8 @@ extension MotionObservableConvertible { can be used to create any operator that only needs to modify values. All state events are forwarded along. */ - func _nextOperator(_ name: String? = nil, args: [Any]? = nil, operation: @escaping (T, (U) -> Void) -> Void, coreAnimation: @escaping (CoreAnimationChannelEvent, CoreAnimationChannel?) -> Void) -> MotionObservable { - return MotionObservable(self.metadata.createChild(Metadata(name, args: args), type: .constraint)) { observer in + func _nextOperator(operation: @escaping (T, (U) -> Void) -> Void, coreAnimation: @escaping (CoreAnimationChannelEvent, CoreAnimationChannel?) -> Void) -> MotionObservable { + return MotionObservable { observer in return self.subscribe(next: { return operation($0, observer.next) }, coreAnimation: { diff --git a/src/operators/foundation/_remember.swift b/src/operators/foundation/_remember.swift index 8ee63b1..beedc6d 100644 --- a/src/operators/foundation/_remember.swift +++ b/src/operators/foundation/_remember.swift @@ -35,7 +35,7 @@ extension MotionObservableConvertible { var lastCoreAnimationEvent: CoreAnimationChannelEvent? var lastVisualizationView: UIView? - return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint))) { observer in + return MotionObservable { observer in if observers.count == 0 { subscription = self.subscribe(next: { value in lastValue = value diff --git a/src/operators/gestures/centroid.swift b/src/operators/gestures/centroid.swift index 0d67d3e..b0d225b 100644 --- a/src/operators/gestures/centroid.swift +++ b/src/operators/gestures/centroid.swift @@ -21,7 +21,7 @@ extension MotionObservableConvertible where T: UIGestureRecognizer { /** Extract centroid from the incoming gesture recognizer. */ public func centroid(in view: UIView) -> MotionObservable { - return _map(#function, args: [view]) { value in + return _map { value in value.location(in: view) } } diff --git a/src/operators/gestures/rotated.swift b/src/operators/gestures/rotated.swift index b777ee2..8333743 100644 --- a/src/operators/gestures/rotated.swift +++ b/src/operators/gestures/rotated.swift @@ -27,7 +27,7 @@ extension MotionObservableConvertible where T: UIRotationGestureRecognizer { var cachedInitialRotation: CGFloat? var lastInitialRotation: CGFloat? - return MotionObservable(metadata.createChild(Metadata(#function, type: .constraint, args: [initialRotation]))) { observer in + return MotionObservable { observer in let initialRotationSubscription = initialRotation.subscribeToValue { lastInitialRotation = $0 } let upstreamSubscription = self.subscribeAndForward(to: observer) { value in diff --git a/src/operators/gestures/scaled.swift b/src/operators/gestures/scaled.swift index d4bb6e8..d7549d9 100644 --- a/src/operators/gestures/scaled.swift +++ b/src/operators/gestures/scaled.swift @@ -27,7 +27,7 @@ extension MotionObservableConvertible where T: UIPinchGestureRecognizer { var cachedInitialScale: CGFloat? var lastInitialScale: CGFloat? - return MotionObservable(metadata.createChild(Metadata(#function, type: .constraint, args: [initialScale]))) { observer in + return MotionObservable { observer in let initialScaleSubscription = initialScale.subscribeToValue { lastInitialScale = $0 } let upstreamSubscription = self.subscribeAndForward(to: observer) { value in diff --git a/src/operators/gestures/translation.swift b/src/operators/gestures/translation.swift index 8452964..ac9f3d6 100644 --- a/src/operators/gestures/translation.swift +++ b/src/operators/gestures/translation.swift @@ -20,7 +20,7 @@ import UIKit extension MotionObservableConvertible where T: UIPanGestureRecognizer { public func translation(in view: UIView) -> MotionObservable { - return _map(#function, args: [view]) { + return _map { return $0.translation(in: view) } } diff --git a/src/operators/gestures/translationAddedTo.swift b/src/operators/gestures/translationAddedTo.swift index ac7721d..f1a5a35 100644 --- a/src/operators/gestures/translationAddedTo.swift +++ b/src/operators/gestures/translationAddedTo.swift @@ -27,7 +27,7 @@ extension MotionObservableConvertible where T: UIPanGestureRecognizer { var cachedInitialPosition: CGPoint? var lastInitialPosition: CGPoint? - return MotionObservable(metadata.createChild(Metadata(#function, type: .constraint, args: [initialPosition, view]))) { observer in + return MotionObservable { observer in let initialPositionSubscription = initialPosition.subscribeToValue { lastInitialPosition = $0 } let upstreamSubscription = self.subscribeAndForward(to: observer) { value in diff --git a/src/operators/gestures/velocity.swift b/src/operators/gestures/velocity.swift index 58e186f..762a2c4 100644 --- a/src/operators/gestures/velocity.swift +++ b/src/operators/gestures/velocity.swift @@ -21,7 +21,7 @@ extension MotionObservableConvertible where T: UIPanGestureRecognizer { /** Extract translational velocity from the incoming pan gesture recognizer. */ public func velocity(in view: UIView) -> MotionObservable { - return _map(#function, args: [view]) { value in + return _map { value in value.velocity(in: view) } } @@ -31,7 +31,7 @@ extension MotionObservableConvertible where T: UIRotationGestureRecognizer { /** Extract rotational velocity from the incoming rotation gesture recognizer. */ public func velocity() -> MotionObservable { - return _map(#function) { value in value.velocity } + return _map { value in value.velocity } } } @@ -39,6 +39,6 @@ extension MotionObservableConvertible where T: UIPinchGestureRecognizer { /** Extract scale velocity from the incoming pinch gesture recognizer. */ public func velocity() -> MotionObservable { - return _map(#function) { value in value.velocity } + return _map { value in value.velocity } } } diff --git a/src/operators/gestures/whenRecognitionStateIs.swift b/src/operators/gestures/whenRecognitionStateIs.swift index 46030f9..15fb55f 100644 --- a/src/operators/gestures/whenRecognitionStateIs.swift +++ b/src/operators/gestures/whenRecognitionStateIs.swift @@ -21,20 +21,20 @@ extension MotionObservableConvertible where T: UIGestureRecognizer { /** Only forwards the gesture recognizer if its state matches the provided value. */ public func whenRecognitionState(is state: UIGestureRecognizerState) -> MotionObservable { - return _filter(#function, args: [state]) { value in + return _filter { value in return value.state == state } } /** Only forwards the gesture recognizer if its state matches any of the provided values. */ public func whenRecognitionState(isAnyOf states: [UIGestureRecognizerState]) -> MotionObservable { - return _filter(#function, args: [states]) { value in + return _filter { value in return states.contains(value.state) } } public func asMotionState() -> MotionObservable { - return _nextOperator(#function) { value, next in + return _nextOperator { value, next in if value is UITapGestureRecognizer { if value.state == .recognized { // Tap gestures are momentary, so we won't have another opportunity to send an .atRest event @@ -53,13 +53,13 @@ extension MotionObservableConvertible where T: UIGestureRecognizer { } public func active() -> MotionObservable { - return _map(#function) { value in + return _map { value in return value.state == .began || value.state == .changed } } public func atRest() -> MotionObservable { - return _map(#function) { value in + return _map { value in return value.state != .began && value.state != .changed } } diff --git a/src/operators/inverted.swift b/src/operators/inverted.swift index 3b825c7..d85a338 100644 --- a/src/operators/inverted.swift +++ b/src/operators/inverted.swift @@ -22,7 +22,7 @@ extension MotionObservableConvertible where T == Bool { Emits the negation of the upstream Boolean value. */ public func inverted() -> MotionObservable { - return _map(#function) { value in + return _map { value in return !value } } @@ -34,6 +34,6 @@ extension MotionObservableConvertible where T: Invertible { Emits the inversion of the upstream value. */ public func inverted() -> MotionObservable { - return _map(#function) { $0.inverted() } + return _map { $0.inverted() } } } diff --git a/src/operators/log.swift b/src/operators/log.swift index a248649..b74ea3f 100644 --- a/src/operators/log.swift +++ b/src/operators/log.swift @@ -24,7 +24,7 @@ extension MotionObservableConvertible { - parameter context: An optional string to be printed before the value. */ public func log(_ context: String? = nil) -> MotionObservable { - return _nextOperator(#function, args: [context as Any], operation: { value, next in + return _nextOperator(operation: { value, next in if let context = context { print(context, value) } else { diff --git a/src/operators/lowerBound.swift b/src/operators/lowerBound.swift index 27986fd..2b628bf 100644 --- a/src/operators/lowerBound.swift +++ b/src/operators/lowerBound.swift @@ -22,7 +22,7 @@ extension MotionObservableConvertible where T: Comparable { Emits either the incoming value or the provided minValue, whichever is larger. */ public func lowerBound(_ minValue: T) -> MotionObservable { - return _map(#function, args: [minValue]) { + return _map { return Swift.max($0, minValue) } } diff --git a/src/operators/merge.swift b/src/operators/merge.swift index 860fcbf..10c74d5 100644 --- a/src/operators/merge.swift +++ b/src/operators/merge.swift @@ -22,7 +22,7 @@ extension MotionObservableConvertible { Emits values as it receives them, both from upstream and from the provided stream. */ public func merge(with stream: O) -> MotionObservable where O: MotionObservableConvertible, O.T == T { - return MotionObservable(Metadata(#function, args: [stream])) { observer in + return MotionObservable { observer in let upstreamSubscription = self.asStream().subscribeAndForward(to: observer) let subscription = stream.asStream().subscribeAndForward(to: observer) return { diff --git a/src/operators/normalizedBy.swift b/src/operators/normalizedBy.swift index 763fcfa..4a1f4b4 100644 --- a/src/operators/normalizedBy.swift +++ b/src/operators/normalizedBy.swift @@ -23,7 +23,7 @@ extension MotionObservableConvertible where T == CGFloat { Emits the incoming value / amount. */ public func normalized(by amount: CGFloat) -> MotionObservable { - return _map(#function, args: [amount]) { + return _map { $0 / amount } } @@ -35,7 +35,7 @@ extension MotionObservableConvertible where T == CGPoint { Emits the incoming value / amount. */ public func normalized(by amount: CGSize) -> MotionObservable { - return _map(#function, args: [amount]) { + return _map { return CGPoint(x: $0.x / amount.width, y: $0.y / amount.height) } diff --git a/src/operators/offsetBy.swift b/src/operators/offsetBy.swift index 85fcfd7..66a635d 100644 --- a/src/operators/offsetBy.swift +++ b/src/operators/offsetBy.swift @@ -23,7 +23,7 @@ extension MotionObservableConvertible where T == CGFloat { Emits the incoming value + amount. */ public func offset(by amount: CGFloat) -> MotionObservable { - return _map(#function, args: [amount]) { + return _map { $0 + amount } } diff --git a/src/operators/rewrite.swift b/src/operators/rewrite.swift index 26cf888..d2b5ec7 100644 --- a/src/operators/rewrite.swift +++ b/src/operators/rewrite.swift @@ -22,7 +22,7 @@ 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(#function, args: [values]) { value, next in + return _nextOperator { value, next in if let mappedValue = values[value] { next(mappedValue) } @@ -33,7 +33,7 @@ extension MotionObservableConvertible where T: Hashable { Emits the mapped value for each incoming value, if one exists, otherwise emits nothing. */ public func rewrite(_ values: [T: O]) -> MotionObservable where O.T == U { - return _nextOperator(#function, args: [values]) { value, next in + return _nextOperator { value, next in if let mappedValue = values[value], let value = mappedValue._read() { next(value) } diff --git a/src/operators/rewriteRange.swift b/src/operators/rewriteRange.swift index 931f1c4..3778e60 100644 --- a/src/operators/rewriteRange.swift +++ b/src/operators/rewriteRange.swift @@ -28,7 +28,7 @@ extension MotionObservableConvertible where T: Subtractable, T: Lerpable { destinationEnd: U ) -> MotionObservable where U: Lerpable, U: Subtractable, U: Addable { - return _map(#function, args: [start, end, destinationStart, destinationEnd]) { + return _map { let position = $0 - start let vector = end - start diff --git a/src/operators/rewriteTo.swift b/src/operators/rewriteTo.swift index 2a6969b..24bdadc 100644 --- a/src/operators/rewriteTo.swift +++ b/src/operators/rewriteTo.swift @@ -22,13 +22,13 @@ extension MotionObservableConvertible { Emit a constant value each time this operator receives a value. */ public func rewriteTo(_ value: U) -> MotionObservable { - return _map(#function, args: [value]) { _ in value } + return _map { _ in value } } /** Emit a constant value each time this operator receives a value. */ public func rewriteTo(_ value: O) -> MotionObservable { - return _map(#function, args: [value]) { _ in value._read()! } + return _map { _ in value._read()! } } } diff --git a/src/operators/rubberBanded.swift b/src/operators/rubberBanded.swift index 839ac5b..d581752 100644 --- a/src/operators/rubberBanded.swift +++ b/src/operators/rubberBanded.swift @@ -23,7 +23,7 @@ extension MotionObservableConvertible where T == CGFloat { Applies resistance to values that fall outside of the given range. */ public func rubberBanded(below: CGFloat, above: CGFloat, maxLength: CGFloat) -> MotionObservable { - return _map(#function, args: [below, above, maxLength]) { + return _map { return rubberBand(value: $0, min: below, max: above, bandLength: maxLength) } } @@ -37,7 +37,7 @@ extension MotionObservableConvertible where T == CGPoint { Does not modify the value if CGRect is .null. */ public func rubberBanded(outsideOf rect: CGRect, maxLength: CGFloat) -> MotionObservable { - return _map(#function, args: [rect, maxLength]) { + return _map { guard rect != .null else { return $0 } @@ -56,7 +56,7 @@ extension MotionObservableConvertible where T == CGPoint { var lastRect: CGRect? var lastMaxLength: CGFloat? var lastValue: CGPoint? - return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [rectStream, maxLengthStream]))) { observer in + return MotionObservable { observer in let checkAndEmit = { guard let rect = lastRect, let maxLength = lastMaxLength, let value = lastValue else { diff --git a/src/operators/scaledBy.swift b/src/operators/scaledBy.swift index 2ca7c7f..8ef6354 100644 --- a/src/operators/scaledBy.swift +++ b/src/operators/scaledBy.swift @@ -23,7 +23,7 @@ extension MotionObservableConvertible where T == CGFloat { Emits the incoming value * amount. */ public func scaled(by amount: CGFloat) -> MotionObservable { - return _map(#function, args: [amount]) { + return _map { $0 * amount } } @@ -34,7 +34,7 @@ extension MotionObservableConvertible where T == CGFloat { public func scaled(by amount: MotionObservable) -> MotionObservable { var lastValue: CGFloat? var amountValue: CGFloat? - return MotionObservable(Metadata(#function, args: [amount])) { observer in + return MotionObservable { observer in let checkAndEmit = { guard let amount = amountValue, let value = lastValue else { return } observer.next(value * amount) diff --git a/src/operators/slop.swift b/src/operators/slop.swift index 6a94f9f..d216fd5 100644 --- a/src/operators/slop.swift +++ b/src/operators/slop.swift @@ -50,7 +50,7 @@ extension MotionObservableConvertible where T == CGFloat { public func slop(size: CGFloat) -> MotionObservable { let size = abs(size) - return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [size]))) { observer in + return MotionObservable { observer in let upstreamSubscription = self .thresholdRange(min: -size, max: size) .rewrite([.below: .onExit, .within: .onReturn, .above: .onExit]) diff --git a/src/operators/startWith.swift b/src/operators/startWith.swift index 4dac810..e0c2eee 100644 --- a/src/operators/startWith.swift +++ b/src/operators/startWith.swift @@ -25,7 +25,7 @@ extension MotionObservableConvertible { The returned stream is therefor guaranteed to always immediately emit a value upon subscription. */ public func startWith(_ value: T) -> MotionObservable { - return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [value]))) { observer in + return MotionObservable { observer in observer.next(value) return self.asStream().subscribeAndForward(to: observer).unsubscribe }._remember() @@ -33,7 +33,7 @@ extension MotionObservableConvertible { @available(*, deprecated, message: "Use startWith() instead.") public func initialValue(_ value: T) -> MotionObservable { - return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [value]))) { observer in + return MotionObservable { observer in observer.next(value) return self.asStream().subscribeAndForward(to: observer).unsubscribe } diff --git a/src/operators/threshold.swift b/src/operators/threshold.swift index e275c8b..c0756a2 100644 --- a/src/operators/threshold.swift +++ b/src/operators/threshold.swift @@ -44,7 +44,7 @@ extension MotionObservableConvertible where T: Comparable { - paramater threshold: The position of the threshold. */ public func threshold(_ threshold: T) -> MotionObservable { - return _nextOperator(#function, args: [threshold]) { value, next in + return _nextOperator { value, next in if value < threshold { next(.below) diff --git a/src/operators/thresholdRange.swift b/src/operators/thresholdRange.swift index 30fd46f..abdc716 100644 --- a/src/operators/thresholdRange.swift +++ b/src/operators/thresholdRange.swift @@ -25,7 +25,7 @@ extension MotionObservableConvertible where T: Comparable { - paramater max: The maximum threshold. */ public func thresholdRange(min: T, max: T) -> MotionObservable { - return _nextOperator(#function, args: [min, max]) { value, next in + return _nextOperator { value, next in if value < min { next(.below) diff --git a/src/operators/toString.swift b/src/operators/toString.swift index aeb53f8..e8a4a73 100644 --- a/src/operators/toString.swift +++ b/src/operators/toString.swift @@ -22,7 +22,7 @@ extension MotionObservableConvertible { Emits a string representation of the incoming value. */ public func toString() -> MotionObservable { - return _map(#function) { String(describing: $0) } + return _map { String(describing: $0) } } } @@ -34,6 +34,6 @@ extension MotionObservableConvertible where T == CGFloat { The incoming value may optionally be formatted according to the provided format string. */ public func toString(format: String) -> MotionObservable { - return _map(#function) { String(format: format, $0) } + return _map { String(format: format, $0) } } } diff --git a/src/operators/upperBound.swift b/src/operators/upperBound.swift index 469279b..3a52455 100644 --- a/src/operators/upperBound.swift +++ b/src/operators/upperBound.swift @@ -22,7 +22,7 @@ extension MotionObservableConvertible where T: Comparable { Emits either the incoming value or the provided maxValue, whichever is smaller. */ public func upperBound(_ maxValue: T) -> MotionObservable { - return _map(#function, args: [maxValue]) { + return _map { return Swift.min($0, maxValue) } } diff --git a/src/operators/valve.swift b/src/operators/valve.swift index 77d40a6..808f788 100644 --- a/src/operators/valve.swift +++ b/src/operators/valve.swift @@ -25,7 +25,7 @@ extension MotionObservableConvertible { when the valveStream emits false. */ public func valve(openWhenTrue valveStream: O) -> MotionObservable where O.T == Bool { - return MotionObservable(Metadata(#function, args: [valveStream])) { observer in + return MotionObservable { observer in var upstreamSubscription: Subscription? let valveSubscription = valveStream.subscribeToValue { shouldOpen in diff --git a/src/operators/visualize.swift b/src/operators/visualize.swift index 835cdce..3931837 100644 --- a/src/operators/visualize.swift +++ b/src/operators/visualize.swift @@ -31,7 +31,7 @@ extension MotionObservableConvertible { This operator assumes that the label will be added to a MotionRuntime's `visualizationView`. */ public func visualize(_ prefix: String? = nil, in view: UIView) -> MotionObservable { - return MotionObservable(Metadata(#function, args: [prefix as Any, view])) { observer in + return MotionObservable { observer in let label = UILabel() let highlight = UIView() highlight.backgroundColor = .white diff --git a/src/operators/x.swift b/src/operators/x.swift index 4dc9561..b9c1977 100644 --- a/src/operators/x.swift +++ b/src/operators/x.swift @@ -23,8 +23,6 @@ extension MotionObservableConvertible where T == CGPoint { Extract the x value from a CGPoint. */ public func x() -> MotionObservable { - return _map(#function, transformVelocity: true) { - $0.x - } + return _map(transformVelocity: true) { $0.x } } } diff --git a/src/operators/xLockedTo.swift b/src/operators/xLockedTo.swift index 04b9a12..25acc6f 100644 --- a/src/operators/xLockedTo.swift +++ b/src/operators/xLockedTo.swift @@ -23,7 +23,7 @@ extension MotionObservableConvertible where T == CGPoint { Lock the point's x value to the given value. */ public func xLocked(to xValue: CGFloat) -> MotionObservable { - return _map(#function, args: [xValue]) { + return _map { .init(x: xValue, y: $0.y) } } @@ -34,7 +34,7 @@ extension MotionObservableConvertible where T == CGPoint { public func xLocked(to xValueStream: O) -> MotionObservable where O.T == CGFloat { var lastUpstreamValue: CGPoint? var lastXValue: CGFloat? - return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [xValueStream]))) { observer in + return MotionObservable { observer in let checkAndEmit = { guard let lastUpstreamValue = lastUpstreamValue, let lastXValue = lastXValue else { return } diff --git a/src/operators/y.swift b/src/operators/y.swift index 6c1d205..c95fef6 100644 --- a/src/operators/y.swift +++ b/src/operators/y.swift @@ -23,8 +23,6 @@ extension MotionObservableConvertible where T == CGPoint { Extract the y value from a CGPoint. */ public func y() -> MotionObservable { - return _map(#function, transformVelocity: true) { - $0.y - } + return _map(transformVelocity: true) { $0.y } } } diff --git a/src/operators/yLockedTo.swift b/src/operators/yLockedTo.swift index 0dcbc94..0de1fcf 100644 --- a/src/operators/yLockedTo.swift +++ b/src/operators/yLockedTo.swift @@ -23,8 +23,6 @@ extension MotionObservableConvertible where T == CGPoint { Lock the point's y value to the given value. */ public func yLocked(to yValue: CGFloat) -> MotionObservable { - return _map(#function, args: [yValue]) { - .init(x: $0.x, y: yValue) - } + return _map { .init(x: $0.x, y: yValue) } } } diff --git a/src/protocols/Stateful.swift b/src/protocols/Stateful.swift index d675330..789afed 100644 --- a/src/protocols/Stateful.swift +++ b/src/protocols/Stateful.swift @@ -58,7 +58,7 @@ final class AggregateMotionState { */ func observe(state: O, withRuntime runtime: MotionRuntime) where O: MotionObservableConvertible, O: AnyObject, O.T == MotionState { let identifier = ObjectIdentifier(state) - runtime.connect(state.asStream().dedupe(), to: ReactiveProperty("Aggregate state", initialValue: .atRest) { state in + runtime.connect(state.asStream().dedupe(), to: ReactiveProperty(initialValue: .atRest) { state in if state == .active { self.activeStates.insert(identifier) } else { @@ -72,6 +72,6 @@ final class AggregateMotionState { return state.asStream() } - private let state = createProperty("state", withInitialValue: MotionState.atRest) + private let state = createProperty(withInitialValue: MotionState.atRest) private var activeStates = Set() } diff --git a/src/reactivetypes/MotionObservable.swift b/src/reactivetypes/MotionObservable.swift index f1fd910..6bb0fb3 100644 --- a/src/reactivetypes/MotionObservable.swift +++ b/src/reactivetypes/MotionObservable.swift @@ -113,28 +113,6 @@ public typealias VisualizationChannel = (UIView) -> Void Throughout this documentation we will treat the words "observable" and "stream" as synonyms. */ public final class MotionObservable: IndefiniteObservable> { - - /** - Creates a new motion observable with the provided metadata and connect function. - - The connect function will be invoked each time this observable is subscribed to. - */ - public init(_ metadata: Metadata, connect: @escaping Connect>) { - self.metadata = metadata - super.init(connect) - } - - /** - The provided name is used to create this observable's Metadata information. - */ - public convenience init(_ name: String? = nil, args: [Any]? = nil, connect: @escaping Connect>) { - self.init(Metadata(name, args: args), connect: connect) - } - - /** - The metadata describing this stream. - */ - public let metadata: Metadata } /** @@ -165,7 +143,7 @@ public final class MotionObserver: Observer { /** A MotionObservableConvertible has a canonical MotionObservable that it can return. */ -public protocol MotionObservableConvertible: Inspectable { +public protocol MotionObservableConvertible { associatedtype T /** diff --git a/src/reactivetypes/Reactive+CALayer.swift b/src/reactivetypes/Reactive+CALayer.swift index ce5ac4b..cfe1d28 100644 --- a/src/reactivetypes/Reactive+CALayer.swift +++ b/src/reactivetypes/Reactive+CALayer.swift @@ -21,10 +21,9 @@ extension Reactive where O: CALayer { public var anchorPoint: ReactiveProperty { let layer = _object return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: layer.anchorPoint, - externalWrite: { layer.anchorPoint = $0 }, - keyPath: "anchorPoint") + return createCoreAnimationProperty(initialValue: layer.anchorPoint, + externalWrite: { layer.anchorPoint = $0 }, + keyPath: "anchorPoint") } } @@ -33,7 +32,7 @@ extension Reactive where O: CALayer { let position = self.position let layer = _object return _properties.named(#function) { - return .init("\(pretty(layer)).\(#function)", initialValue: .init(anchorPoint: anchorPoint.value, position: position.value)) { + return .init(initialValue: .init(anchorPoint: anchorPoint.value, position: position.value)) { anchorPoint.value = $0.anchorPoint; position.value = $0.position } } @@ -42,117 +41,106 @@ extension Reactive where O: CALayer { public var cornerRadius: ReactiveProperty { let layer = _object return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: layer.cornerRadius, - externalWrite: { layer.cornerRadius = $0 }, - keyPath: "cornerRadius") + return createCoreAnimationProperty(initialValue: layer.cornerRadius, + externalWrite: { layer.cornerRadius = $0 }, + keyPath: "cornerRadius") } } public var height: ReactiveProperty { let size = self.size return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: size.value.height, - externalWrite: { var dimensions = size.value; dimensions.height = $0; size.value = dimensions }, - keyPath: "bounds.size.height") + return createCoreAnimationProperty(initialValue: size.value.height, + externalWrite: { var dimensions = size.value; dimensions.height = $0; size.value = dimensions }, + keyPath: "bounds.size.height") } } public var opacity: ReactiveProperty { let layer = _object return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: CGFloat(layer.opacity), - externalWrite: { layer.opacity = Float($0) }, - keyPath: "opacity") + return createCoreAnimationProperty(initialValue: CGFloat(layer.opacity), + externalWrite: { layer.opacity = Float($0) }, + keyPath: "opacity") } } public var position: ReactiveProperty { let layer = _object return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: layer.position, - externalWrite: { layer.position = $0 }, - keyPath: "position") + return createCoreAnimationProperty(initialValue: layer.position, + externalWrite: { layer.position = $0 }, + keyPath: "position") } } public var positionX: ReactiveProperty { let position = self.position return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: position.value.x, - externalWrite: { var point = position.value; point.x = $0; position.value = point }, - keyPath: "position.x") + return createCoreAnimationProperty(initialValue: position.value.x, + externalWrite: { var point = position.value; point.x = $0; position.value = point }, + keyPath: "position.x") } } public var positionY: ReactiveProperty { let position = self.position return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: position.value.y, - externalWrite: { var point = position.value; point.y = $0; position.value = point }, - keyPath: "position.y") + return createCoreAnimationProperty(initialValue: position.value.y, + externalWrite: { var point = position.value; point.y = $0; position.value = point }, + keyPath: "position.y") } } public var rotation: ReactiveProperty { let layer = _object return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: layer.value(forKeyPath: "transform.rotation.z") as! CGFloat, - externalWrite: { layer.setValue($0, forKeyPath: "transform.rotation.z") }, - keyPath: "transform.rotation.z") + return createCoreAnimationProperty(initialValue: layer.value(forKeyPath: "transform.rotation.z") as! CGFloat, + externalWrite: { layer.setValue($0, forKeyPath: "transform.rotation.z") }, + keyPath: "transform.rotation.z") } } public var scale: ReactiveProperty { let layer = _object return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: layer.value(forKeyPath: "transform.scale") as! CGFloat, - externalWrite: { layer.setValue($0, forKeyPath: "transform.scale") }, - keyPath: "transform.scale.xy") + return createCoreAnimationProperty(initialValue: layer.value(forKeyPath: "transform.scale") as! CGFloat, + externalWrite: { layer.setValue($0, forKeyPath: "transform.scale") }, + keyPath: "transform.scale.xy") } } public var size: ReactiveProperty { let layer = _object return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: layer.bounds.size, - externalWrite: { layer.bounds.size = $0 }, - keyPath: "bounds.size") + return createCoreAnimationProperty(initialValue: layer.bounds.size, + externalWrite: { layer.bounds.size = $0 }, + keyPath: "bounds.size") } } public var shadowPath: ReactiveProperty { let layer = _object return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: layer.shadowPath!, - externalWrite: { layer.shadowPath = $0 }, - keyPath: "shadowPath") + return createCoreAnimationProperty(initialValue: layer.shadowPath!, + externalWrite: { layer.shadowPath = $0 }, + keyPath: "shadowPath") } } public var width: ReactiveProperty { let size = self.size return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: size.value.width, - externalWrite: { var dimensions = size.value; dimensions.width = $0; size.value = dimensions }, - keyPath: "bounds.size.width") + return createCoreAnimationProperty(initialValue: size.value.width, + externalWrite: { var dimensions = size.value; dimensions.width = $0; size.value = dimensions }, + keyPath: "bounds.size.width") } } - func createCoreAnimationProperty(_ name: String, initialValue: T, externalWrite: @escaping NextChannel, keyPath: String) -> ReactiveProperty { + func createCoreAnimationProperty(initialValue: T, externalWrite: @escaping NextChannel, keyPath: String) -> ReactiveProperty { let layer = _object var decomposedKeys = Set() - let property = ReactiveProperty("\(pretty(layer)).\(name)", initialValue: initialValue, externalWrite: { value in + let property = ReactiveProperty(initialValue: initialValue, externalWrite: { value in let actionsWereDisabled = CATransaction.disableActions() CATransaction.setDisableActions(true) externalWrite(value) diff --git a/src/reactivetypes/Reactive+CAShapeLayer.swift b/src/reactivetypes/Reactive+CAShapeLayer.swift index c109b19..d9e5338 100644 --- a/src/reactivetypes/Reactive+CAShapeLayer.swift +++ b/src/reactivetypes/Reactive+CAShapeLayer.swift @@ -23,10 +23,9 @@ extension Reactive where O: CAShapeLayer { public var path: ReactiveProperty { let layer = _object return _properties.named(#function) { - return createCoreAnimationProperty(#function, - initialValue: layer.path!, - externalWrite: { layer.path = $0 }, - keyPath: "path") + return createCoreAnimationProperty(initialValue: layer.path!, + externalWrite: { layer.path = $0 }, + keyPath: "path") } } diff --git a/src/reactivetypes/Reactive+UIButton.swift b/src/reactivetypes/Reactive+UIButton.swift index 835c341..50b68dc 100644 --- a/src/reactivetypes/Reactive+UIButton.swift +++ b/src/reactivetypes/Reactive+UIButton.swift @@ -53,6 +53,4 @@ public final class ReactiveButtonTarget: NSObject { didHighlightObservers.forEach { $0.next(false) } } private var didHighlightObservers: [MotionObserver] = [] - - public let metadata = Metadata("Button target") } diff --git a/src/reactivetypes/Reactive+UILabel.swift b/src/reactivetypes/Reactive+UILabel.swift index 080298b..d82bb4b 100644 --- a/src/reactivetypes/Reactive+UILabel.swift +++ b/src/reactivetypes/Reactive+UILabel.swift @@ -21,7 +21,7 @@ extension Reactive where O: UILabel { public var text: ReactiveProperty { let view = _object return _properties.named(#function) { - return .init("\(pretty(view)).\(#function)", initialValue: view.text ?? "") { + return .init(initialValue: view.text ?? "") { view.text = $0 } } diff --git a/src/reactivetypes/Reactive+UIView.swift b/src/reactivetypes/Reactive+UIView.swift index 76c8d75..0a6785e 100644 --- a/src/reactivetypes/Reactive+UIView.swift +++ b/src/reactivetypes/Reactive+UIView.swift @@ -21,7 +21,7 @@ extension Reactive where O: UIView { public var isUserInteractionEnabled: ReactiveProperty { let view = _object return _properties.named(#function) { - return .init("\(pretty(view)).\(#function)", initialValue: view.isUserInteractionEnabled) { + return .init(initialValue: view.isUserInteractionEnabled) { view.isUserInteractionEnabled = $0 } } @@ -30,7 +30,7 @@ extension Reactive where O: UIView { public var backgroundColor: ReactiveProperty { let view = _object return _properties.named(#function) { - return .init("\(pretty(view)).\(#function)", initialValue: view.backgroundColor!) { + return .init(initialValue: view.backgroundColor!) { view.backgroundColor = $0 } } @@ -39,7 +39,7 @@ extension Reactive where O: UIView { public var alpha: ReactiveProperty { let view = _object return _properties.named(#function) { - return .init("\(pretty(view)).\(#function)", initialValue: view.alpha) { + return .init(initialValue: view.alpha) { view.alpha = $0 } } diff --git a/src/reactivetypes/ReactiveScrollViewDelegate.swift b/src/reactivetypes/ReactiveScrollViewDelegate.swift index 6a35ded..ee68e04 100644 --- a/src/reactivetypes/ReactiveScrollViewDelegate.swift +++ b/src/reactivetypes/ReactiveScrollViewDelegate.swift @@ -56,6 +56,4 @@ public final class ReactiveScrollViewDelegate: NSObject, UIScrollViewDelegate, M didScrollObservers.forEach { $0.next(scrollView) } } private var didScrollObservers: [MotionObserver] = [] - - public let metadata = Metadata("Scroll view delegate") } diff --git a/src/reactivetypes/ReactiveUIGestureRecognizer.swift b/src/reactivetypes/ReactiveUIGestureRecognizer.swift index 86b29e4..063ae13 100644 --- a/src/reactivetypes/ReactiveUIGestureRecognizer.swift +++ b/src/reactivetypes/ReactiveUIGestureRecognizer.swift @@ -21,8 +21,7 @@ public class ReactiveUIGestureRecognizer: Stateful { public lazy var isEnabled: ReactiveProperty = { let gestureRecognizer = self.gestureRecognizer - return ReactiveProperty(#function, - initialValue: gestureRecognizer.isEnabled, + return ReactiveProperty(initialValue: gestureRecognizer.isEnabled, externalWrite: { gestureRecognizer.isEnabled = $0 }) }() @@ -32,8 +31,6 @@ public class ReactiveUIGestureRecognizer: Stateful { self.stream = gestureToStream(gestureRecognizer) } - public let metadata = Metadata("Gesture Recognizer") - public var state: MotionObservable { return asStream().asMotionState() } diff --git a/src/systems/coreAnimationPathTweenToStream.swift b/src/systems/coreAnimationPathTweenToStream.swift index 5f88386..b5b9e46 100644 --- a/src/systems/coreAnimationPathTweenToStream.swift +++ b/src/systems/coreAnimationPathTweenToStream.swift @@ -20,7 +20,7 @@ import IndefiniteObservable /** Create a core animation tween system for a Tween plan. */ public func coreAnimation(_ tween: PathTweenShadow) -> MotionObservable { - return MotionObservable(Metadata("Core Animation Path Tween", args: [tween.duration, tween.delay, tween.path, tween.timeline as Any, tween.enabled, tween.state])) { observer in + return MotionObservable { observer in var subscriptions: [Subscription] = [] let key = NSUUID().uuidString diff --git a/src/systems/coreAnimationSpringToStream.swift b/src/systems/coreAnimationSpringToStream.swift index 1a7bb51..cfb88c9 100644 --- a/src/systems/coreAnimationSpringToStream.swift +++ b/src/systems/coreAnimationSpringToStream.swift @@ -24,7 +24,7 @@ import UIKit */ public func coreAnimation(_ spring: SpringShadow) -> (MotionObservable) where T: Subtractable { let initialVelocityStream = spring.initialVelocity.asStream() - return MotionObservable(Metadata("Core Animation Spring", args: [spring.enabled, spring.state, spring.initialValue, spring.initialVelocity, spring.destination, spring.tension, spring.friction, spring.mass, spring.suggestedDuration, spring.threshold])) { observer in + return MotionObservable { observer in var animationKeys: [String] = [] var to: T? diff --git a/src/systems/coreAnimationTweenToStream.swift b/src/systems/coreAnimationTweenToStream.swift index f63b6f9..023215a 100644 --- a/src/systems/coreAnimationTweenToStream.swift +++ b/src/systems/coreAnimationTweenToStream.swift @@ -35,7 +35,7 @@ public func coreAnimation(_ tween: TweenShadow) -> MotionObservable whe } private func streamFromTween(_ tween: TweenShadow, configureEvent: @escaping (CoreAnimationChannelAdd) -> CoreAnimationChannelAdd) -> MotionObservable { - return MotionObservable(Metadata("Core Animation Tween", args: [tween])) { observer in + return MotionObservable { observer in var animationKeys: [String] = [] var activeAnimations = Set() diff --git a/src/systems/gestureToStream.swift b/src/systems/gestureToStream.swift index fece489..062d0fa 100644 --- a/src/systems/gestureToStream.swift +++ b/src/systems/gestureToStream.swift @@ -19,7 +19,7 @@ import UIKit /** Create a gesture source that will connect to the provided gesture recognizer. */ func gestureToStream(_ gesture: T) -> MotionObservable { - return MotionObservable(Metadata("Gesture Recognizer", args: [gesture])) { observer in + return MotionObservable { observer in return GestureConnection(subscribedTo: gesture, observer: observer).disconnect } } diff --git a/src/systems/scrollViewToStream.swift b/src/systems/scrollViewToStream.swift index 45b9b00..ac30317 100644 --- a/src/systems/scrollViewToStream.swift +++ b/src/systems/scrollViewToStream.swift @@ -23,7 +23,7 @@ import UIKit This scroll source will not emit state updates. */ func scrollViewToStream(_ scrollView: UIScrollView) -> MotionObservable { - return MotionObservable(Metadata("Scroll View", args: [scrollView])) { observer in + return MotionObservable { observer in return ScrollViewConnection(subscribedTo: scrollView, observer: observer).disconnect } } diff --git a/src/systems/sliderToStream.swift b/src/systems/sliderToStream.swift index 6a46cd2..9c63935 100644 --- a/src/systems/sliderToStream.swift +++ b/src/systems/sliderToStream.swift @@ -23,7 +23,7 @@ import UIKit This scroll source will not emit state updates. */ func sliderToStream(_ slider: UISlider) -> MotionObservable { - return MotionObservable(Metadata("Slider", args: [slider])) { observer in + return MotionObservable { observer in return SliderConnection(subscribedTo: slider, observer: observer).disconnect } } diff --git a/src/timeline/Timeline.swift b/src/timeline/Timeline.swift index 2bc4896..8368206 100644 --- a/src/timeline/Timeline.swift +++ b/src/timeline/Timeline.swift @@ -35,7 +35,7 @@ public final class Timeline { Unpausing a timeline should allow all associated interactions to continue progressing in time on their own, starting from timeOffset. */ - public let paused = createProperty("Timeline.paused", withInitialValue: false) + public let paused = createProperty(withInitialValue: false) /** The starting time for all interactions associated with this timeline. @@ -47,9 +47,7 @@ public final class Timeline { Only affects associated interactions if the timeline is paused. */ - public let timeOffset = createProperty("Timeline.timeOffset", withInitialValue: 0) - - public let metadata = Metadata("Timeline") + public let timeOffset = createProperty(withInitialValue: 0) } extension Timeline: MotionObservableConvertible { @@ -80,7 +78,7 @@ extension Timeline: MotionObservableConvertible { Returns a stream representation of the Timeline. */ public func asStream() -> MotionObservable { - return MotionObservable(metadata) { observer in + return MotionObservable { observer in var paused = self.paused.value var timeOffset = self.timeOffset.value diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index 5ac3ca7..4895ffe 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -100,7 +100,7 @@ public final class TransitionContext: NSObject { fore: UIViewController, gestureRecognizers: Set, presentationController: UIPresentationController?) { - self.direction = createProperty("Transition.direction", withInitialValue: direction) + self.direction = createProperty(withInitialValue: direction) self.initialDirection = direction self.back = back self.fore = fore diff --git a/tests/unit/MotionRuntimeTests.swift b/tests/unit/MotionRuntimeTests.swift index 45c0722..77a70de 100644 --- a/tests/unit/MotionRuntimeTests.swift +++ b/tests/unit/MotionRuntimeTests.swift @@ -18,8 +18,8 @@ import XCTest import MaterialMotion public class MockTween: Togglable, Stateful { - let _state = createProperty("Tween._state", withInitialValue: MotionState.atRest) - public let enabled = createProperty("Tween.enabled", withInitialValue: true) + let _state = createProperty(withInitialValue: MotionState.atRest) + public let enabled = createProperty(withInitialValue: true) public func setState(state: MotionState) { _state.value = state } diff --git a/tests/unit/ReactivePropertyTests.swift b/tests/unit/ReactivePropertyTests.swift index b51b12c..a898d24 100644 --- a/tests/unit/ReactivePropertyTests.swift +++ b/tests/unit/ReactivePropertyTests.swift @@ -91,7 +91,7 @@ class ReactivePropertyTests: XCTestCase { func testCoreAnimation() { let didReceiveEvent = expectation(description: "Did receive event") - let property = ReactiveProperty("test", initialValue: 10, externalWrite: { _ in }, coreAnimation: { event in + let property = ReactiveProperty(initialValue: 10, externalWrite: { _ in }, coreAnimation: { event in didReceiveEvent.fulfill() }) @@ -121,7 +121,7 @@ class ReactivePropertyTests: XCTestCase { XCTAssertTrue(Reactive(view).isUserInteractionEnabled === Reactive(view).isUserInteractionEnabled) } - func testPropertiesReleasedWhenDereferenced() { + func testPropertiesNotReleasedWhenDereferenced() { let view = UIView() var prop1: ReactiveProperty? = Reactive(view).isUserInteractionEnabled let objectIdentifier = ObjectIdentifier(prop1!) @@ -129,7 +129,7 @@ class ReactivePropertyTests: XCTestCase { prop1 = nil let prop2 = Reactive(view).isUserInteractionEnabled - XCTAssertTrue(objectIdentifier != ObjectIdentifier(prop2)) + XCTAssertTrue(objectIdentifier == ObjectIdentifier(prop2)) } func testObjectRetainedByReactiveType() { From 715c97222ab8c73c2432f01033d1a3b12bd616b8 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 12 May 2017 12:45:53 -0400 Subject: [PATCH 41/60] Remove all deprecated APIs in preparation for major release. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator Reviewed By: randcode-generator Tags: #material_motion Differential Revision: http://codereview.cc/D3216 --- src/MotionRuntime.swift | 16 ---------------- src/interactions/Tossable.swift | 5 ----- src/operators/startWith.swift | 8 -------- src/reactivetypes/Reactive+UIView.swift | 6 ------ src/transitions/TransitionController.swift | 8 -------- tests/unit/operator/startWithTests.swift | 16 ---------------- 6 files changed, 59 deletions(-) diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift index 35b9fe4..39a0bdc 100644 --- a/src/MotionRuntime.swift +++ b/src/MotionRuntime.swift @@ -93,22 +93,6 @@ public final class MotionRuntime { return interactions.flatMap { $0 as? I } } - @available(*, deprecated, message: "Use interactions(ofType:for:) instead.") - public func interactions(for target: I.Target, ofType: I.Type) -> [I] where I: Interaction, I.Target: AnyObject { - guard let interactions = targets[ObjectIdentifier(target)] else { - return [] - } - return interactions.flatMap { $0 as? I } - } - - @available(*, deprecated, message: "Use interactions(ofType:for:) instead.") - public func interactions(for target: I.Target, filter: (Any) -> I?) -> [I] where I: Interaction, I.Target: AnyObject { - guard let interactions = targets[ObjectIdentifier(target)] else { - return [] - } - return interactions.flatMap { $0 as? I } - } - /** Creates a toggling association between one interaction's state and the other interaction's enabling. diff --git a/src/interactions/Tossable.swift b/src/interactions/Tossable.swift index aa0d6f9..b5e226c 100644 --- a/src/interactions/Tossable.swift +++ b/src/interactions/Tossable.swift @@ -86,9 +86,4 @@ public final class Tossable: Interaction, Stateful { } let aggregateState = AggregateMotionState() - - @available(*, deprecated, message: "Use init(spring:draggable:) instead.") - public convenience init(system: @escaping SpringToStream, draggable: Draggable = Draggable()) { - self.init(spring: Spring(threshold: 1, system: system), draggable: draggable) - } } diff --git a/src/operators/startWith.swift b/src/operators/startWith.swift index e0c2eee..f0bebcb 100644 --- a/src/operators/startWith.swift +++ b/src/operators/startWith.swift @@ -30,12 +30,4 @@ extension MotionObservableConvertible { return self.asStream().subscribeAndForward(to: observer).unsubscribe }._remember() } - - @available(*, deprecated, message: "Use startWith() instead.") - public func initialValue(_ value: T) -> MotionObservable { - return MotionObservable { observer in - observer.next(value) - return self.asStream().subscribeAndForward(to: observer).unsubscribe - } - } } diff --git a/src/reactivetypes/Reactive+UIView.swift b/src/reactivetypes/Reactive+UIView.swift index 0a6785e..b12ac84 100644 --- a/src/reactivetypes/Reactive+UIView.swift +++ b/src/reactivetypes/Reactive+UIView.swift @@ -49,10 +49,4 @@ extension Reactive where O: UIView { let view = _object return Reactive(view.layer) } - - @available(*, deprecated, message: "Use layer instead.") - public var reactiveLayer: Reactive { - let view = _object - return Reactive(view.layer) - } } diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index cd41c1d..7bb8a6c 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -147,14 +147,6 @@ public final class TransitionController { _transitioningDelegate = TransitioningDelegate(viewController: viewController) } - /** - Deprecated. Please use methods directly on the transitionController instead. - */ - @available(*, deprecated, message: "Please use methods directly on the transitionController instead.") - public var dismisser: ViewControllerDismisser { - return _transitioningDelegate.dismisser - } - fileprivate let _transitioningDelegate: TransitioningDelegate } diff --git a/tests/unit/operator/startWithTests.swift b/tests/unit/operator/startWithTests.swift index 7ce5a6e..c56e705 100644 --- a/tests/unit/operator/startWithTests.swift +++ b/tests/unit/operator/startWithTests.swift @@ -77,20 +77,4 @@ class startWithTests: XCTestCase { subscription.unsubscribe() secondSubscription.unsubscribe() } - - @available(*, deprecated) - func testDeprecatedInitialValueIsReceivedFirst() { - let property = createProperty() - - var values: [CGFloat] = [] - let subscription = property.initialValue(10).subscribeToValue { value in - values.append(value) - } - - property.value = -10 - - XCTAssertEqual(values, [10, 0, -10]) - - subscription.unsubscribe() - } } From ad99804c4c0c3723e0d3ab7e6ee5a8f7581fee73 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 12 May 2017 12:52:26 -0400 Subject: [PATCH 42/60] Resolve new Xcode 8.3.2 warnings. Summary: Warning was "warning: value was defined but never used; consider replacing with boolean test" Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator Reviewed By: randcode-generator Tags: #material_motion Differential Revision: http://codereview.cc/D3217 --- src/reactivetypes/Reactive+CALayer.swift | 1 - src/transitions/TransitionController.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/reactivetypes/Reactive+CALayer.swift b/src/reactivetypes/Reactive+CALayer.swift index cfe1d28..09f2359 100644 --- a/src/reactivetypes/Reactive+CALayer.swift +++ b/src/reactivetypes/Reactive+CALayer.swift @@ -30,7 +30,6 @@ extension Reactive where O: CALayer { public var anchorPointAdjustment: ReactiveProperty { let anchorPoint = self.anchorPoint let position = self.position - let layer = _object return _properties.named(#function) { return .init(initialValue: .init(anchorPoint: anchorPoint.value, position: position.value)) { anchorPoint.value = $0.anchorPoint; position.value = $0.position diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index 7bb8a6c..04b59ec 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -67,7 +67,7 @@ public final class TransitionController { set { _transitioningDelegate.transition = newValue - if let transition = newValue as? TransitionWithPresentation { + if newValue is TransitionWithPresentation { _transitioningDelegate.associatedViewController?.modalPresentationStyle = .custom } } From b3908da854352e882b6084a7939e00175d57097b Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 12 May 2017 13:57:06 -0400 Subject: [PATCH 43/60] When emitting Tweens with a delay, set the fill mode to backward. Summary: This ensures that the animation's initial value will be visible on the property before the animation begins. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Subscribers: markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3218 --- src/systems/coreAnimationTweenToStream.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/systems/coreAnimationTweenToStream.swift b/src/systems/coreAnimationTweenToStream.swift index 023215a..5e2f50a 100644 --- a/src/systems/coreAnimationTweenToStream.swift +++ b/src/systems/coreAnimationTweenToStream.swift @@ -66,6 +66,9 @@ private func streamFromTween(_ tween: TweenShadow, configureEvent: @escapi return } animation.beginTime = CFTimeInterval(tween.delay.value) + if tween.delay.value > 0 { + animation.fillMode = kCAFillModeBackwards + } animation.duration = CFTimeInterval(duration) animation.repeatCount = Float(tween.repeatCount.value) animation.repeatDuration = CFTimeInterval(tween.repeatDuration.value) From fa28ef906310d1ae6794959037d48de98339227b Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 12 May 2017 13:58:34 -0400 Subject: [PATCH 44/60] Also slow down the beginTime for tweens when simulator slow motion is enabled. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3219 --- src/reactivetypes/Reactive+CALayer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/src/reactivetypes/Reactive+CALayer.swift b/src/reactivetypes/Reactive+CALayer.swift index 09f2359..b27257a 100644 --- a/src/reactivetypes/Reactive+CALayer.swift +++ b/src/reactivetypes/Reactive+CALayer.swift @@ -154,6 +154,7 @@ extension Reactive where O: CALayer { let animation = info.animation.copy() as! CAPropertyAnimation animation.duration *= TimeInterval(simulatorDragCoefficient()) + animation.beginTime *= TimeInterval(simulatorDragCoefficient()) if layer.speed == 0, let lastTimelineState = layer.lastTimelineState { animation.beginTime = TimeInterval(lastTimelineState.beginTime) + animation.beginTime From c0b341672856aa7447f339688bba067e9868679d Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 12 May 2017 14:30:15 -0400 Subject: [PATCH 45/60] Add support for pre/post delay to TransitionTween. Summary: This allows the transition tween to delay the tween based on the direction of the transition. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3220 --- src/interactions/TransitionTween.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/interactions/TransitionTween.swift b/src/interactions/TransitionTween.swift index 79b3ed3..aa2c2f4 100644 --- a/src/interactions/TransitionTween.swift +++ b/src/interactions/TransitionTween.swift @@ -57,6 +57,8 @@ public final class TransitionTween: Tween { public init(duration: CGFloat, forwardValues: [T], direction: ReactiveProperty, + delayBefore: CGFloat = 0, + delayAfter: CGFloat = 0, forwardKeyPositions: [CGFloat] = [], system: @escaping TweenToStream = coreAnimation, timeline: Timeline? = nil) { @@ -70,6 +72,7 @@ public final class TransitionTween: Tween { self.toggledValues = direction.dedupe().rewrite([.backward: backwardValues, .forward: forwardValues]) self.toggledOffsets = direction.dedupe().rewrite([.backward: backwardKeyPositions, .forward: forwardKeyPositions]) + self.toggledDelay = direction.dedupe().rewrite([.backward: delayAfter, .forward: delayBefore]) super.init(duration: duration, values: values, system: system, timeline: timeline) } @@ -93,6 +96,7 @@ public final class TransitionTween: Tween { constraints: ConstraintApplicator? = nil) { let unlocked = createProperty(withInitialValue: false) runtime.connect(direction.dedupe().rewriteTo(false), to: unlocked) + runtime.connect(toggledDelay, to: delay) runtime.connect(toggledValues, to: values) runtime.connect(toggledOffsets, to: offsets) super.add(to: property, withRuntime: runtime) { @@ -108,4 +112,5 @@ public final class TransitionTween: Tween { private let direction: ReactiveProperty private let toggledValues: MotionObservable<[T]> private let toggledOffsets: MotionObservable<[CGFloat]> + private let toggledDelay: MotionObservable } From 9d796f374bfb859f5bff1170ca2c1da407373a9d Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 16 May 2017 12:02:04 -0400 Subject: [PATCH 46/60] Don't attempt to slow down CASpringAnimation animations when slow-motion animations is enabled. Summary: CASpringAnimation can't be slowed down by modifying its duration. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, appsforartists Reviewed By: O2 Material Motion, #material_motion, appsforartists Subscribers: markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3224 --- src/reactivetypes/Reactive+CALayer.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/reactivetypes/Reactive+CALayer.swift b/src/reactivetypes/Reactive+CALayer.swift index b27257a..0528982 100644 --- a/src/reactivetypes/Reactive+CALayer.swift +++ b/src/reactivetypes/Reactive+CALayer.swift @@ -153,8 +153,10 @@ extension Reactive where O: CALayer { let animation = info.animation.copy() as! CAPropertyAnimation - animation.duration *= TimeInterval(simulatorDragCoefficient()) - animation.beginTime *= TimeInterval(simulatorDragCoefficient()) + if !(animation is CASpringAnimation) { + animation.duration *= TimeInterval(simulatorDragCoefficient()) + animation.beginTime *= TimeInterval(simulatorDragCoefficient()) + } if layer.speed == 0, let lastTimelineState = layer.lastTimelineState { animation.beginTime = TimeInterval(lastTimelineState.beginTime) + animation.beginTime From 483d52bf017a4c760c83a0d0a6150559d9c5f9e8 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 16 May 2017 15:04:42 -0400 Subject: [PATCH 47/60] Update CocoaPods to 1.2.1. --- Podfile.lock | 2 +- .../Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 58a2b25..f89291a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -21,4 +21,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f503265a0d60526a0d28c96dd4bdcfb40fb562fc -COCOAPODS: 1.2.0 +COCOAPODS: 1.2.1 diff --git a/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj index 872aeaa..3e75619 100644 --- a/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj @@ -597,7 +597,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; FF0DFE3F30076BF4A0E6EC6C /* [CP] Check Pods Manifest.lock */ = { @@ -612,7 +612,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ From 4ad2c55e1cc1f9b5efd50f39195b0d23085b2af4 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 16 May 2017 11:32:57 -0400 Subject: [PATCH 48/60] Make TransitionWithPresentation's method an instance method. Summary: We're moving away from static methods on the transition type. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator Reviewed By: randcode-generator Tags: #material_motion Differential Revision: http://codereview.cc/D3227 --- src/transitions/Transition.swift | 6 +++--- src/transitions/TransitionController.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/transitions/Transition.swift b/src/transitions/Transition.swift index c37e229..e6646d0 100644 --- a/src/transitions/Transition.swift +++ b/src/transitions/Transition.swift @@ -57,9 +57,9 @@ public protocol TransitionWithPresentation: Transition { The returned presentation controller may choose to conform to WillBeginTransition in order to associate reactive motion with the transition. */ - static func presentationController(forPresented presented: UIViewController, - presenting: UIViewController?, - source: UIViewController) -> UIPresentationController + func presentationController(forPresented presented: UIViewController, + presenting: UIViewController?, + source: UIViewController) -> UIPresentationController } /** diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index 04b59ec..c949c97 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -249,9 +249,9 @@ private final class TransitioningDelegate: NSObject, UIViewControllerTransitioni if let presentationController = presentationController { return presentationController } - presentationController = type(of: transitionWithPresentation).presentationController(forPresented: presented, - presenting: presenting, - source: source) + presentationController = transitionWithPresentation.presentationController(forPresented: presented, + presenting: presenting, + source: source) return presentationController } } From 12acff94e9d0070014c41dd39a7083d57dd4ff5f Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 16 May 2017 11:32:10 -0400 Subject: [PATCH 49/60] Add support for fallback transitions. Summary: This new API allows transitions to fallback to other transition types in certain contexts. For example, a full-screen fab transition might choose to fall back to a modal slide transition when being dismissed. In order to check sameness we had to make Transition a class protocol. This doesn't affect any existing transition implementations because they are all classes in practice. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator Reviewed By: randcode-generator Tags: #material_motion Differential Revision: http://codereview.cc/D3228 --- src/transitions/Transition.swift | 16 +++++++++++++++- src/transitions/TransitionContext.swift | 8 ++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/transitions/Transition.swift b/src/transitions/Transition.swift index e6646d0..9bcf89d 100644 --- a/src/transitions/Transition.swift +++ b/src/transitions/Transition.swift @@ -21,7 +21,7 @@ import UIKit A transition is responsible for describing the motion that will occur during a UIViewController transition. */ -public protocol Transition { +public protocol Transition: class { /** Invoked on initiation of a view controller transition. @@ -30,6 +30,20 @@ public protocol Transition { func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] } +/** + A transition can return an alternative fallback transition instance. + */ +public protocol TransitionWithFallback: Transition { + /** + Invoked before the transition begins. + + If the returned instance also conforms to TransitionWithFallback, then the returned instance's + fallback will be queried. This repeats until a returned instance does not conform to + TransitionWithFallback or it returns self. + */ + func fallbackTansition(withContext ctx: TransitionContext) -> Transition +} + /** A transition is responsible for describing the motion that will occur during a UIViewController transition. diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index 4895ffe..37cd120 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -111,6 +111,14 @@ public final class TransitionContext: NSObject { self.transition = transition super.init() + + while let fallbackTransition = self.transition as? TransitionWithFallback { + let fallback = fallbackTransition.fallbackTansition(withContext: self) + if fallback === self.transition { + break + } + self.transition = fallback + } } fileprivate let initialDirection: TransitionDirection From 5a6570a1e4fe8eef29d3fcffd4d1219fdff47590 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 16 May 2017 12:31:27 -0400 Subject: [PATCH 50/60] Allow TransitionWithPresentation to return nil for conditional presentation. Summary: Returning nil means no presentation controller will be used. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator Reviewed By: randcode-generator Tags: #material_motion Differential Revision: http://codereview.cc/D3229 --- src/transitions/Transition.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/transitions/Transition.swift b/src/transitions/Transition.swift index 9bcf89d..332c650 100644 --- a/src/transitions/Transition.swift +++ b/src/transitions/Transition.swift @@ -70,10 +70,12 @@ public protocol TransitionWithPresentation: Transition { The returned presentation controller may choose to conform to WillBeginTransition in order to associate reactive motion with the transition. + + If nil is returned then no presentation controller will be used. */ func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, - source: UIViewController) -> UIPresentationController + source: UIViewController) -> UIPresentationController? } /** From c11949ebad03e306a755325e4f27bfe517485bdf Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 16 May 2017 14:56:06 -0400 Subject: [PATCH 51/60] Add a defaultModalPresentationStyle API for transitions with presentation. Summary: This allows transitions with presentation to choose the default modal presentation style for a view controller. This is necessary because the transition may choose not to use a presentation controller even though it conforms to the TransitionWithPresentation protocol (e.g. a modal transition that covers the whole screen may not need a presentation controller). Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3230 --- src/transitions/Transition.swift | 11 +++++++++++ src/transitions/TransitionController.swift | 5 +++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/transitions/Transition.swift b/src/transitions/Transition.swift index 332c650..a0138ac 100644 --- a/src/transitions/Transition.swift +++ b/src/transitions/Transition.swift @@ -62,6 +62,17 @@ public protocol TransitionWithTermination: Transition { */ public protocol TransitionWithPresentation: Transition { + /** + The modal presentation style this transition expects to use. + + This value is queried when the transition is assigned to a view controller's + transitionController. The result, if any, is assigned to the view controller's + modalPresentationStyle property. + + In order for a presentationController to be used, this method should return `.custom`. + */ + func defaultModalPresentationStyle() -> UIModalPresentationStyle? + /** Queried before the Transition object is instantiated and only once, when the fore view controller is initially presented. diff --git a/src/transitions/TransitionController.swift b/src/transitions/TransitionController.swift index c949c97..36f42fb 100644 --- a/src/transitions/TransitionController.swift +++ b/src/transitions/TransitionController.swift @@ -67,8 +67,9 @@ public final class TransitionController { set { _transitioningDelegate.transition = newValue - if newValue is TransitionWithPresentation { - _transitioningDelegate.associatedViewController?.modalPresentationStyle = .custom + if let presentationTransition = newValue as? TransitionWithPresentation, + let modalPresentationStyle = presentationTransition.defaultModalPresentationStyle() { + _transitioningDelegate.associatedViewController?.modalPresentationStyle = modalPresentationStyle } } get { return _transitioningDelegate.transition } From 9c40e0b8e045312a1c9b256f8fd166a86e70bb2d Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Wed, 17 May 2017 13:06:58 -0400 Subject: [PATCH 52/60] Add a backgroundColor property to Reactive+CALayer. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3233 --- src/reactivetypes/Reactive+CALayer.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/reactivetypes/Reactive+CALayer.swift b/src/reactivetypes/Reactive+CALayer.swift index 0528982..3f87799 100644 --- a/src/reactivetypes/Reactive+CALayer.swift +++ b/src/reactivetypes/Reactive+CALayer.swift @@ -37,6 +37,15 @@ extension Reactive where O: CALayer { } } + public var backgroundColor: ReactiveProperty { + let layer = _object + return _properties.named(#function) { + return createCoreAnimationProperty(initialValue: layer.backgroundColor!, + externalWrite: { layer.backgroundColor = $0 }, + keyPath: "backgroundColor") + } + } + public var cornerRadius: ReactiveProperty { let layer = _object return _properties.named(#function) { From 7ae1b04bbd458c65c7c4d09152c90464ef2fd5bc Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 18 May 2017 14:03:40 -0400 Subject: [PATCH 53/60] Move fallback calculations to later in the transition lifecycle. Summary: This ensures that fallback calculations have access to the containerView property. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Reviewed By: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, markwei Tags: #material_motion Differential Revision: http://codereview.cc/D3239 --- src/transitions/TransitionContext.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/transitions/TransitionContext.swift b/src/transitions/TransitionContext.swift index 37cd120..e076d8a 100644 --- a/src/transitions/TransitionContext.swift +++ b/src/transitions/TransitionContext.swift @@ -111,14 +111,6 @@ public final class TransitionContext: NSObject { self.transition = transition super.init() - - while let fallbackTransition = self.transition as? TransitionWithFallback { - let fallback = fallbackTransition.fallbackTansition(withContext: self) - if fallback === self.transition { - break - } - self.transition = fallback - } } fileprivate let initialDirection: TransitionDirection @@ -182,6 +174,16 @@ extension TransitionContext { self.runtime = MotionRuntime(containerView: containerView()) self.replicator.containerView = containerView() + // We query the fallback just before initiating the transition so that the transition context is + // primed with the content view and other transition-related information. + while let fallbackTransition = self.transition as? TransitionWithFallback { + let fallback = fallbackTransition.fallbackTansition(withContext: self) + if fallback === self.transition { + break + } + self.transition = fallback + } + pokeSystemAnimations() var terminators = transition.willBeginTransition(withContext: self, runtime: self.runtime) From e0090c9c0175a1dd2cee7a20edde951faed577bf Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 18 May 2017 16:38:19 -0400 Subject: [PATCH 54/60] Attempt to reduce the flakiness of the PropertiesNotReleasedWhenDereferenced test. Summary: Closes https://github.com/material-motion/material-motion-swift/issues/119 Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, appsforartists Reviewed By: O2 Material Motion, #material_motion, appsforartists Tags: #material_motion Differential Revision: http://codereview.cc/D3241 --- tests/unit/ReactivePropertyTests.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/unit/ReactivePropertyTests.swift b/tests/unit/ReactivePropertyTests.swift index a898d24..e387605 100644 --- a/tests/unit/ReactivePropertyTests.swift +++ b/tests/unit/ReactivePropertyTests.swift @@ -123,10 +123,12 @@ class ReactivePropertyTests: XCTestCase { func testPropertiesNotReleasedWhenDereferenced() { let view = UIView() - var prop1: ReactiveProperty? = Reactive(view).isUserInteractionEnabled - let objectIdentifier = ObjectIdentifier(prop1!) - prop1 = nil + var objectIdentifier: ObjectIdentifier! + autoreleasepool { + let prop1 = Reactive(view).isUserInteractionEnabled + objectIdentifier = ObjectIdentifier(prop1) + } let prop2 = Reactive(view).isUserInteractionEnabled XCTAssertTrue(objectIdentifier == ObjectIdentifier(prop2)) From 40decaf295aa1e41417e7f4dec8a7391c29e27ab Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 18 May 2017 17:10:15 -0400 Subject: [PATCH 55/60] Update modal dialog example to make use of a presentation controller. Reviewers: O2 Material Motion, O4 Material Apple platform reviewers, #material_motion, randcode-generator Reviewed By: randcode-generator Tags: #material_motion Differential Revision: http://codereview.cc/D3242 --- examples/ModalDialogExample.swift | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/examples/ModalDialogExample.swift b/examples/ModalDialogExample.swift index a027bb3..482f5fd 100644 --- a/examples/ModalDialogExample.swift +++ b/examples/ModalDialogExample.swift @@ -46,8 +46,6 @@ class ModalDialogViewController: UIViewController { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) transitionController.transition = ModalDialogTransition() - preferredContentSize = .init(width: 200, height: 200) - modalPresentationStyle = .overCurrentContext } required init?(coder aDecoder: NSCoder) { @@ -67,7 +65,19 @@ class ModalDialogViewController: UIViewController { } } -class ModalDialogTransition: SelfDismissingTransition { +class ModalDialogTransition: SelfDismissingTransition, TransitionWithPresentation { + + public func defaultModalPresentationStyle() -> UIModalPresentationStyle? { + return .custom + } + + public func presentationController(forPresented presented: UIViewController, + presenting: UIViewController?, + source: UIViewController) -> UIPresentationController? { + return ModalDialogPresentationController(presentedViewController: presented, + presenting: presenting) + } + func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] { let size = ctx.fore.view.frame.size @@ -113,3 +123,18 @@ class ModalDialogTransition: SelfDismissingTransition { dismisser.dismissWhenGestureRecognizerBegins(pan) } } + +private final class ModalDialogPresentationController: UIPresentationController { + + override var frameOfPresentedViewInContainerView: CGRect { + guard let containerView = containerView else { + assertionFailure("Missing container view during frame query.") + return .zero() + } + let size = CGSize(width: 200, height: 200) + return CGRect(x: containerView.bounds.midX - size.width / 2, + y: containerView.bounds.midY - size.height / 2, + width: size.width, + height: size.height) + } +} From 67082003ffcf4783dbe8eea92c91363b66cded7f Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 18 May 2017 17:31:25 -0400 Subject: [PATCH 56/60] Automatic changelog preparation for release. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0746d97..199e8bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# #develop# + + TODO: Enumerate changes. + + # 1.3.0 Highlights: From eedf8eb2b361617a485cd0b68d3de523254b3c1c Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 18 May 2017 18:09:09 -0400 Subject: [PATCH 57/60] Initial pass at release notes. --- CHANGELOG.md | 317 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 315 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199e8bd..a19a67a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,320 @@ -# #develop# +# 2.0.0 - TODO: Enumerate changes. +## Breaking changes +• The `IndefiniteObservable` dependency has been bumped to 4.0.0. [View the release notes](https://github.com/material-motion/indefinite-observable-swift/releases/tag/v4.0.0). + +• The `Metadata` and `Inspectable` types have been removed from Material Motion. All related APIs have been simplified accordingly. + +• The `MotionRuntime.toGraphViz` API has been removed. There is no replacement API. + +• `Tossable`'s `init(system:draggable:)` has been removed. Use `init(spring:draggable:)` instead. + +• `SelfDismissingTransition`'s `willPresent(fore:dismisser:)` is no longer a static method. Implement the method as an instance method instead. + +• `Transition` is now a class protocol. This means that only object-types can conform to `Transition`. + +• `TransitionController`'s `dismisser` has been removed. All methods have been moved directly to the TransitionController object. + +• `Tween`'s `keyPositions` has been removed. Use `offsets` instead. + +• `MotionRuntime`'s `interactions(for:filter:)` has been removed. Use `interactions(ofType:for:)` instead. + +## New features + +### Reactive architecture + +• Subscriptions no longer automatically unsubscribe when the Subscription object is released. Subscriptions will stay active for as long as the head of the stream is alive. + +• Reactive types are now global and shared across all instances of MotionRuntime. You can use `Reactive(object)` to fetch a cached reactive version of a supported type. + +• MotionRuntime now supports a `.get` for UISlider instances. This will return an Observable of the slider's value. + +• New operator `ignoreUntil`. + +• New reactive variant of operator `rubberBanded(outsideOf:maxLength:)`. + +• New operator for float types `toString(format:)`. + +• New `ReactiveScrollViewDelegate` API turns UIScrollViewDelegate events into observable streams. + +For example, the delegate itself is an observable on the scroll view's content offset: + +```swift +let delegate = ReactiveScrollViewDelegate() +scrollView.delegate = delegate +delegate.x().subscribeToValue { contentOffset in + print(contentOffset) +} +``` + +• New `ReactiveButtonTarget` API for building reactive UIButtons. + +• `MotionRuntime` has a new API `start(_:when:is:)` for starting interactions when another interaction reaches a given state. + +• `MotionRuntime` has a new `isBeingManipulated` stream. This stream emits true when any `Manipulable` interaction becomes active and false when all `Manipulable` interactions come to rest. + +### Interactions + +• `MotionRuntime` now has a new `isBeingManipulated` property that indicates whether any manipulation interaction is active. + +Any interaction that conforms to the new `Manipulation` type will affect the runtime's `isBeingManipulated` property. + +• `Draggable` now has a `resistance` property that can be used to create drag resistance beyond a draggable region. + +```swift +draggable.resistance.perimeter.value = someRect +``` + +• `Tween` has new properties for creating repeating animations: `repeatCount`, `repeatDuration`, and `autoreverses`. + +These properties directly map to the corresponding properties in Core Animation. + +### Transitions + +• New `TransitionWithFallback` protocol allows transitions to swap themselves out for another transition instance. + +• New `TransitionWithPresentation` protocol allows transitions to customize their presentation using an iOS presentation controller. See the modal dialog case study for an example of using this new functionality. + +• New `TransitionWithTermination` protocol allows transitions to perform cleanup logic at the end of a transition. + +• `TransitionContext`'s `gestureRecognizers` is now settable. This makes it possible to add arbitrary gesture recognizers to a transition. + +## Source changes + +* [Attempt to reduce the flakiness of the PropertiesNotReleasedWhenDereferenced test.](https://github.com/material-motion/material-motion-swift/commit/e0090c9c0175a1dd2cee7a20edde951faed577bf) (Jeff Verkoeyen) +* [Move fallback calculations to later in the transition lifecycle.](https://github.com/material-motion/material-motion-swift/commit/7ae1b04bbd458c65c7c4d09152c90464ef2fd5bc) (Jeff Verkoeyen) +* [Add a backgroundColor property to Reactive+CALayer.](https://github.com/material-motion/material-motion-swift/commit/9c40e0b8e045312a1c9b256f8fd166a86e70bb2d) (Jeff Verkoeyen) +* [Add a defaultModalPresentationStyle API for transitions with presentation.](https://github.com/material-motion/material-motion-swift/commit/c11949ebad03e306a755325e4f27bfe517485bdf) (Jeff Verkoeyen) +* [Allow TransitionWithPresentation to return nil for conditional presentation.](https://github.com/material-motion/material-motion-swift/commit/5a6570a1e4fe8eef29d3fcffd4d1219fdff47590) (Jeff Verkoeyen) +* [Add support for fallback transitions.](https://github.com/material-motion/material-motion-swift/commit/12acff94e9d0070014c41dd39a7083d57dd4ff5f) (Jeff Verkoeyen) +* [Make TransitionWithPresentation's method an instance method.](https://github.com/material-motion/material-motion-swift/commit/4ad2c55e1cc1f9b5efd50f39195b0d23085b2af4) (Jeff Verkoeyen) +* [Don't attempt to slow down CASpringAnimation animations when slow-motion animations is enabled.](https://github.com/material-motion/material-motion-swift/commit/9d796f374bfb859f5bff1170ca2c1da407373a9d) (Jeff Verkoeyen) +* [Add support for pre/post delay to TransitionTween.](https://github.com/material-motion/material-motion-swift/commit/c0b341672856aa7447f339688bba067e9868679d) (Jeff Verkoeyen) +* [Also slow down the beginTime for tweens when simulator slow motion is enabled.](https://github.com/material-motion/material-motion-swift/commit/fa28ef906310d1ae6794959037d48de98339227b) (Jeff Verkoeyen) +* [When emitting Tweens with a delay, set the fill mode to backward.](https://github.com/material-motion/material-motion-swift/commit/b3908da854352e882b6084a7939e00175d57097b) (Jeff Verkoeyen) +* [Resolve new Xcode 8.3.2 warnings.](https://github.com/material-motion/material-motion-swift/commit/ad99804c4c0c3723e0d3ab7e6ee5a8f7581fee73) (Jeff Verkoeyen) +* [Remove all deprecated APIs in preparation for major release.](https://github.com/material-motion/material-motion-swift/commit/715c97222ab8c73c2432f01033d1a3b12bd616b8) (Jeff Verkoeyen) +* [Remove Metadata.](https://github.com/material-motion/material-motion-swift/commit/fddbc7e888f41e4ac0e10803c365d8116d245b57) (Jeff Verkoeyen) +* [Rename keyPositions to offsets](https://github.com/material-motion/material-motion-swift/commit/8d73d04ff707851e639e11c48f88ec1a363a66e3) (Eric Tang) +* [Add a reactive button target type and an initial isHighlighted stream.](https://github.com/material-motion/material-motion-swift/commit/af75058c6896195a689873eab06613a2adec2798) (Jeff Verkoeyen) +* [Add format support to the toString operator for numerical types.](https://github.com/material-motion/material-motion-swift/commit/6c9d84da86f4d5e614c6e1b9e9cc5baed685a1be) (Jeff Verkoeyen) +* [Add runtime.get for UISlider instances.](https://github.com/material-motion/material-motion-swift/commit/0bcd0f14adcfd4d0f5aa92bfe56647e00372cf2d) (Jeff Verkoeyen) +* [Add reactive UILabel type with text property.](https://github.com/material-motion/material-motion-swift/commit/5bef83680ad50d7b683f7f61e923274e6996ef08) (Jeff Verkoeyen) +* [When using a transition with presentation, use the .custom modal presentation style.](https://github.com/material-motion/material-motion-swift/commit/b0be085ca0a5474e3c27a0ba1bf1029161e28471) (Jeff Verkoeyen) +* [Allow transition types to be instantiated and stored on the transition controller.](https://github.com/material-motion/material-motion-swift/commit/5b6149d5646e3fab1188e8e2c1714d02dc5d0ce8) (Jeff Verkoeyen) +* [Remove the foreAlignmentEdge property from the transition controller.](https://github.com/material-motion/material-motion-swift/commit/235a3e7f3a4b78678b6e841c6e196471436555f4) (Jeff Verkoeyen) +* [Add support for customizing transition presentation.](https://github.com/material-motion/material-motion-swift/commit/d93233e8e433592d6445da9a5bfc382d5f622f44) (Jeff Verkoeyen) +* [Avoid excessive TransitionTween emissions when the transition direction changes.](https://github.com/material-motion/material-motion-swift/commit/33f60d7b260f3c4b9f0c8ad64230bfd68d643e71) (Jeff Verkoeyen) +* [Added ignoreUntil and simplified slop](https://github.com/material-motion/material-motion-swift/commit/86bf12b91ee6d2f1857160a30582221a8a9c4ce7) (Eric Tang) +* [Added unit test](https://github.com/material-motion/material-motion-swift/commit/cb8ba4ff8387d688e47f84cd3e5980ef6ab8d030) (Eric Tang) +* [Swap params for runtime.interactions() API](https://github.com/material-motion/material-motion-swift/commit/7fe07b0f1e3d599aba8727645367df0b553df529) (Eric Tang) +* [Fix build failure.](https://github.com/material-motion/material-motion-swift/commit/98065f2504dec4da267e6f60c7bdd3b7809b3215) (Jeff Verkoeyen) +* [Add a ReactiveScrollViewDelegate and replace usage of the MotionRuntime in the carousel demo.](https://github.com/material-motion/material-motion-swift/commit/db2d5bc8540b282e7dcc72271db62ff3d9565071) (Jeff Verkoeyen) +* [Mark all MotionObservable subscribe methods with @discardableResult.](https://github.com/material-motion/material-motion-swift/commit/ffc8f8c7d3557587721db5e2f1cff0d37ceee0f5) (Jeff Verkoeyen) +* [Add a new Reactive type for querying reactive properties.](https://github.com/material-motion/material-motion-swift/commit/be0ea019c4d2db5ec63e625895f64ec73ca7b702) (Jeff Verkoeyen) +* [Shorten the delayBy test delay.](https://github.com/material-motion/material-motion-swift/commit/67cb7b18dccce117e1a7d248e0919b4f9edb613d) (Jeff Verkoeyen) +* [Added new start function to MotionRuntime](https://github.com/material-motion/material-motion-swift/commit/9c5010e54b8380eae6ce337408aeb1778fc39cc8) (Eric Tang) +* [Reduce flakiness of delay test.](https://github.com/material-motion/material-motion-swift/commit/d584666c449e5a6304a22e8f2fcdcb1d66b6e56a) (Jeff Verkoeyen) +* [Move Timeline to a timeline folder.](https://github.com/material-motion/material-motion-swift/commit/f83c027067fe0ebcc0792d69648362660bcb3279) (Jeff Verkoeyen) +* [Bump IndefiniteObservable to 4.0 and add explicit unsubscriptions to the runtime.](https://github.com/material-motion/material-motion-swift/commit/7adfe179843218713e11001a7839144b0203124f) (Jeff Verkoeyen) +* [Add repeat APIs to Tween](https://github.com/material-motion/material-motion-swift/commit/5022aad2777f16fcb373e2c155e1f0d47ada9a36) (Eric Tang) +* [Change runtime.interactions API to use an ofType: argument instead of a block.](https://github.com/material-motion/material-motion-swift/commit/d6ab9d281ea15f388fe0d099686bb812df3caba8) (Jeff Verkoeyen) +* [Add a resistance property to Draggable.](https://github.com/material-motion/material-motion-swift/commit/dbff13d9018514e67d54742cac1dd4e176ac2b30) (Jeff Verkoeyen) +* [Add a Manipulation type and implement UIKit view controller transitioning interactivity APIs.](https://github.com/material-motion/material-motion-swift/commit/9fa2b22dabc9a7fcd7bfb9a702007db1680d8fd4) (Jeff Verkoeyen) +* [View controllers are only interactive if at least one gesture recognizer is active.](https://github.com/material-motion/material-motion-swift/commit/a921f721ffc5c8b9cc75dc27b589eb949f5c7112) (Jeff Verkoeyen) +* [Ensure that system animations take effect during view controller transitions.](https://github.com/material-motion/material-motion-swift/commit/9b2347005135d11a7c28a5b27af8730c6467724b) (Jeff Verkoeyen) +* [Operators that use _map no longer transform velocity by default.](https://github.com/material-motion/material-motion-swift/commit/dab2e7de73cb8b4485b548b3ca192b9b9484db88) (Jeff Verkoeyen) +* [When no gesture recognizer is provided to a gestural interaction that expects one, the interaction now does nothing.](https://github.com/material-motion/material-motion-swift/commit/d43a5f69cdcf1029273f8873f225845c62a994d1) (Jeff Verkoeyen) +* [Fix build failure.](https://github.com/material-motion/material-motion-swift/commit/3e471897d3b9f2d401ba6d125b480bf1c621405e) (Jeff Verkoeyen) +* [Add foreAlignmentEdge property to TransitionController.](https://github.com/material-motion/material-motion-swift/commit/3498a0f8e7d177e21e89fc2934c371423e79f1f2) (Jeff Verkoeyen) +* [Deprecate transitionController.dismisser and move the APIs into TransitionController.](https://github.com/material-motion/material-motion-swift/commit/e2ad598ae466e4dfe034be751796fd96a82cb37b) (Jeff Verkoeyen) +* [Add missing imports.](https://github.com/material-motion/material-motion-swift/commit/3f16a4d90fc842999fb23b3af7954c69bf6c64a2) (Jeff Verkoeyen) +* [Add missing Foundation import.](https://github.com/material-motion/material-motion-swift/commit/b31c1361ca6744c0c069db98bb6c6b89e3ffbb4b) (Jeff Verkoeyen) +* [Add Togglable conformity to Rotatable and Scalable.](https://github.com/material-motion/material-motion-swift/commit/f90e15b7d62b7f7102e7827a5a850bcbfa7a0451) (Jeff Verkoeyen) + +## API changes + +Auto-generated by running: + + apidiff origin/stable release-candidate swift MaterialMotion.xcworkspace MaterialMotion + +### Debugging + +#### Inspectable + +*removed* protocol: `Inspectable` + +#### Metadata + +*removed* class: `Metadata` + +### Interactions + +#### Draggable + +*new* var: `resistance` in `Draggable` + +*modified* class: `Draggable` + +| Type of change: | Declaration | +|---|---| +| From: | `public final class Draggable : Gesturable, Interaction, Togglable, Stateful` | +| To: | `public final class Draggable : Gesturable, Interaction, Togglable, Manipulation` | + +#### Manipulation + +*new* protocol: `Manipulation` + +#### Rotatable + +*modified* class: `Rotatable` + +| Type of change: | Declaration | +|---|---| +| From: | `public final class Rotatable : Gesturable, Interaction, Togglable, Stateful` | +| To: | `public final class Rotatable : Gesturable, Interaction, Togglable, Manipulation` | + +#### Scalable + +*modified* class: `Scalable` + +| Type of change: | Declaration | +|---|---| +| From: | `public final class Scalable : Gesturable, Interaction, Togglable, Stateful` | +| To: | `public final class Scalable : Gesturable, Interaction, Togglable, Manipulation` | + +#### Tossable + +*removed* method: `init(system:draggable:)` in `Tossable` + +#### Tween + +*new* var: `offsets` in `Tween` + +*new* var: `repeatCount` in `Tween` + +*new* var: `repeatDuration` in `Tween` + +*new* var: `autoreverses` in `Tween` + +*removed* var: `keyPositions` in `Tween` + +### Reactive architecture + +#### MotionObservableConvertible + +*new* method: `ignoreUntil(_:)` in `MotionObservableConvertible` + +*new* method: `rubberBanded(outsideOf:maxLength:)` in `MotionObservableConvertible` + +*new* method: `toString(format:)` in `MotionObservableConvertible` + +#### MotionRuntime + +*new* method: `interactions(ofType:for:)` in `MotionRuntime` + +*new* method: `start(_:when:is:)` in `MotionRuntime` + +*new* var: `isBeingManipulated` in `MotionRuntime` + +*removed* method: `interactions(for:filter:)` in `MotionRuntime` + +*removed* method: `asGraphviz()` in `MotionRuntime` + +#### ReactiveButtonTarget + +*new* class: `ReactiveButtonTarget` + +*new* var: `didHighlight` in `ReactiveButtonTarget` + +#### ReactiveUIView + +*removed* class: `ReactiveUIView` + +#### ReactiveCAShapeLayer + +*removed* class: `ReactiveCAShapeLayer` + +#### ReactiveScrollViewDelegate + +*new* class: `ReactiveScrollViewDelegate` + +### Transitions + +#### SelfDismissingTransition + +*new* method: `willPresent(fore:dismisser:)` in `SelfDismissingTransition` + +*removed* static method: `willPresent(fore:dismisser:)` in `SelfDismissingTransition` + +#### Transition + +*removed* method: `init()` in `Transition` + +*modified* protocol: `Transition` + +| Type of change: | Declaration | +|---|---| +| From: | `public protocol Transition` | +| To: | `public protocol Transition : class` | + +#### TransitionWithFallback + +*new* protocol: `TransitionWithFallback` + +*new* method: `fallbackTansition(withContext:)` in `TransitionWithFallback` + +#### TransitionWithPresentation + +*new* protocol: `TransitionWithPresentation` + +*new* method: `presentationController(forPresented:presenting:source:)` in `TransitionWithPresentation` + +*new* method: `defaultModalPresentationStyle()` in `TransitionWithPresentation` + +#### TransitionWithTermination + +*new* protocol: `TransitionWithTermination` + +*new* method: `didEndTransition(withContext:runtime:)` in `TransitionWithTermination` + +#### TransitionContext + +*modified* var: `gestureRecognizers` in `TransitionContext` + +| Type of change: | Declaration | +|---|---| +| From: | `public var gestureRecognizers: Set { get }` | +| To: | `public let gestureRecognizers: Set` | + +#### TransitionController + +*new* method: `disableSimultaneousRecognition(of:)` in `TransitionController` + +*new* var: `presentationController` in `TransitionController` + +*new* method: `topEdgeDismisserDelegate(for:)` in `TransitionController` + +*new* var: `gestureRecognizers` in `TransitionController` + +*new* var: `transition` in `TransitionController` + +*new* var: `gestureDelegate` in `TransitionController` + +*new* method: `dismissWhenGestureRecognizerBegins(_:)` in `TransitionController` + +*removed* var: `transitionType` in `TransitionController` + +*removed* var: `dismisser` in `TransitionController` + +## Non-source changes + +* [Update modal dialog example to make use of a presentation controller.](https://github.com/material-motion/material-motion-swift/commit/40decaf295aa1e41417e7f4dec8a7391c29e27ab) (Jeff Verkoeyen) +* [Update CocoaPods to 1.2.1.](https://github.com/material-motion/material-motion-swift/commit/483d52bf017a4c760c83a0d0a6150559d9c5f9e8) (Jeff Verkoeyen) +* [Add Discord badge](https://github.com/material-motion/material-motion-swift/commit/7b5e51f534a206f054b40a9a61a1bdd03a8da957) (Brenton Simpson) +* [Remove use of Reactive type in carousel example.](https://github.com/material-motion/material-motion-swift/commit/27edf6b1f12c2587fa096c22ed9d3d484fc0ca39) (Jeff Verkoeyen) +* [Make both push back case studies use a light status bar in the modal view.](https://github.com/material-motion/material-motion-swift/commit/eaecd862aabb193de5a45024f52365fa1fbcb027) (Jeff Verkoeyen) # 1.3.0 From f9984a32243c6a0b460b122e055508b95ec54cb8 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 19 May 2017 11:05:48 -0400 Subject: [PATCH 58/60] Add more release notes to CHANGELOG.md. --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a19a67a..1ae6814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # 2.0.0 +This major release of Material Motion focuses includes improvements to the transitioning architecture, the reactive architecture, and new features on many of the interactions. + ## Breaking changes • The `IndefiniteObservable` dependency has been bumped to 4.0.0. [View the release notes](https://github.com/material-motion/indefinite-observable-swift/releases/tag/v4.0.0). @@ -70,6 +72,14 @@ draggable.resistance.perimeter.value = someRect These properties directly map to the corresponding properties in Core Animation. +• `TransitionTween` has new initializer values `delayBefore` and `delayAfter`. + +`delayBefore` is the delay used when transitioning forward. `delayAfter` is the delay used when transitioning backward. + +• The gesture property for gesture-based interactions is now optional. When nil, the interaction will do nothing. + +This is primarily useful when building transitions that have optional interactivity. + ### Transitions • New `TransitionWithFallback` protocol allows transitions to swap themselves out for another transition instance. From 9e5ac12051e5711b580c3ada6a367b531b8e153d Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 19 May 2017 14:46:09 -0400 Subject: [PATCH 59/60] Bump the release number to v2.0.0. --- .jazzy.yaml | 4 ++-- MaterialMotion.podspec | 2 +- Podfile.lock | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.jazzy.yaml b/.jazzy.yaml index 6ea5976..90a30de 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -1,5 +1,5 @@ module: MaterialMotion -module_version: 1.3.0 +module_version: 2.0.0 sdk: iphonesimulator xcodebuild_arguments: - -workspace @@ -7,4 +7,4 @@ xcodebuild_arguments: - -scheme - MaterialMotion github_url: https://github.com/material-motion/material-motion-swift -github_file_prefix: https://github.com/material-motion/material-motion-swift/tree/v1.3.0 +github_file_prefix: https://github.com/material-motion/material-motion-swift/tree/v2.0.0 diff --git a/MaterialMotion.podspec b/MaterialMotion.podspec index bf4ca1c..18daf65 100644 --- a/MaterialMotion.podspec +++ b/MaterialMotion.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "MaterialMotion" s.summary = "Reactive motion driven by Core Animation." - s.version = "1.3.0" + s.version = "2.0.0" s.authors = "The Material Motion Authors" s.license = "Apache 2.0" s.homepage = "https://github.com/material-motion/material-motion-swift" diff --git a/Podfile.lock b/Podfile.lock index f89291a..a5bdb38 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -3,7 +3,7 @@ PODS: - IndefiniteObservable (4.0.0): - IndefiniteObservable/lib (= 4.0.0) - IndefiniteObservable/lib (4.0.0) - - MaterialMotion (1.3.0): + - MaterialMotion (2.0.0): - IndefiniteObservable (~> 4.0) DEPENDENCIES: @@ -17,7 +17,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: CatalogByConvention: ef0913973b86b4234bcadf22aa84037c4a47cbbd IndefiniteObservable: 1ee6af37efa8763a222cc6d414cd125e26ed9b6a - MaterialMotion: 42e9f95334ad208e9d44536d188d75488d847c81 + MaterialMotion: a412b109cfe4ab7b1fd956409a5da9a3635d3dce PODFILE CHECKSUM: f503265a0d60526a0d28c96dd4bdcfb40fb562fc From a02f525c2add1fe740fb0d9f100bae0beaa19165 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 19 May 2017 15:48:31 -0400 Subject: [PATCH 60/60] Resolve build failures with older versions of Swift. --- src/interactions/DirectlyManipulable.swift | 5 +++-- src/operators/toString.swift | 1 + src/reactivetypes/ReactiveScrollViewDelegate.swift | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/interactions/DirectlyManipulable.swift b/src/interactions/DirectlyManipulable.swift index 72e22a3..8c474c1 100644 --- a/src/interactions/DirectlyManipulable.swift +++ b/src/interactions/DirectlyManipulable.swift @@ -63,7 +63,7 @@ public final class DirectlyManipulable: NSObject, Interaction, Togglable, Statef public func add(to view: UIView, withRuntime runtime: MotionRuntime, constraints: NoConstraints) { for gestureRecognizer in [draggable.nextGestureRecognizer, rotatable.nextGestureRecognizer, - scalable.nextGestureRecognizer] { + scalable.nextGestureRecognizer] as [UIGestureRecognizer?] { if gestureRecognizer?.delegate == nil { gestureRecognizer?.delegate = self } @@ -73,7 +73,8 @@ public final class DirectlyManipulable: NSObject, Interaction, Togglable, Statef runtime.connect(enabled, to: rotatable.enabled) runtime.connect(enabled, to: scalable.enabled) - let anchorPointRecognizers = [rotatable.nextGestureRecognizer, scalable.nextGestureRecognizer].flatMap { $0 } + let gestures: [UIGestureRecognizer?] = [rotatable.nextGestureRecognizer, scalable.nextGestureRecognizer] + let anchorPointRecognizers = gestures.flatMap { $0 } let adjustsAnchorPoint = AdjustsAnchorPoint(gestureRecognizers: anchorPointRecognizers) runtime.add(adjustsAnchorPoint, to: view) diff --git a/src/operators/toString.swift b/src/operators/toString.swift index e8a4a73..faf6da0 100644 --- a/src/operators/toString.swift +++ b/src/operators/toString.swift @@ -15,6 +15,7 @@ */ import Foundation +import CoreGraphics extension MotionObservableConvertible { diff --git a/src/reactivetypes/ReactiveScrollViewDelegate.swift b/src/reactivetypes/ReactiveScrollViewDelegate.swift index ee68e04..de2168c 100644 --- a/src/reactivetypes/ReactiveScrollViewDelegate.swift +++ b/src/reactivetypes/ReactiveScrollViewDelegate.swift @@ -15,6 +15,7 @@ */ import Foundation +import UIKit /** A UIScrollViewDelegate implementation that exposes observable streams for the scroll view delegate