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

Commit

Permalink
Spring shape updated to better support enabling/disabling and state o…
Browse files Browse the repository at this point in the history
…bservation.

Summary:
Spring now has both a state and enabled property. The Spring instance's value stream is multicast, and the underlying system that drives this value stream is expected to update the spring's state property.

This is a precursor to being able to delete the state channel in its entirety. I'll be cleaning up the mechanism by which the spring's value stream is created in a follow-up change.

In order to keep the examples functioning, this change also introduces "terminal conditions" for transitions. This is a hack solution and will need to be iterated on.

Reviewers: O4 Material Apple platform reviewers, appsforartists, #material_motion, O2 Material Motion

Reviewed By: appsforartists, #material_motion, O2 Material Motion

Subscribers: appsforartists

Tags: #material_motion

Differential Revision: http://codereview.cc/D2683
  • Loading branch information
jverkoey committed Feb 16, 2017
1 parent fb7e7ba commit 41fee56
Show file tree
Hide file tree
Showing 15 changed files with 229 additions and 148 deletions.
20 changes: 14 additions & 6 deletions examples/ContextualTransitionExample.swift
Expand Up @@ -275,14 +275,18 @@ private class PushBackTransitionDirector: TransitionDirector {
let movement = spring(back: contextView, fore: foreImageView, transition: transition)
let size = spring(back: contextView.bounds.size, fore: fitSize, threshold: 1, transition: transition)

var terminalStates = [movement.state.asStream(), size.state.asStream()]

let pans = transition.gestureRecognizers.filter { $0 is UIPanGestureRecognizer }.map { $0 as! UIPanGestureRecognizer }
for pan in pans {
let atRestStream = runtime.get(pan).atRest()
movement.compose { $0.valve(openWhenTrue: atRestStream) }
size.compose { $0.valve(openWhenTrue: atRestStream) }
terminalStates.append(runtime.get(pan).asMotionState())

let velocityStream = runtime.get(pan).velocityOnReleaseStream()
movement.add(initialVelocityStream: velocityStream)
runtime.add(velocityStream, to: movement.initialVelocity)

runtime.add(atRestStream, to: movement.enabled)
runtime.add(atRestStream, to: size.enabled)

runtime.add(runtime.get(pan)
.translation(in: runtime.containerView)
Expand All @@ -298,11 +302,15 @@ private class PushBackTransitionDirector: TransitionDirector {

runtime.add(Hidden(), to: foreImageView)

runtime.add(spring(back: 0, fore: 1, threshold: 0.01, transition: transition),
to: runtime.get(transition.fore.view.layer).opacity)
let opacity: TransitionSpring<CGFloat> = spring(back: 0, fore: 1, threshold: 0.01, transition: transition)
runtime.add(opacity, to: runtime.get(transition.fore.view.layer).opacity)

terminalStates.append(opacity.state.asStream())

transition.terminateWhenAllAtRest(terminalStates)
}

private func spring<T where T: Subtractable, T: Zeroable>(back: T, fore: T, threshold: CGFloat, transition: Transition) -> TransitionSpring<T> {
private func spring<T where T: Subtractable, T: Zeroable, T: Equatable>(back: T, fore: T, threshold: CGFloat, transition: Transition) -> TransitionSpring<T> {
let spring = TransitionSpring(back: back, fore: fore, direction: transition.direction, threshold: threshold, system: coreAnimation)
spring.friction.value = 500
spring.tension.value = 1000
Expand Down
5 changes: 4 additions & 1 deletion examples/DragSourceExample.swift
Expand Up @@ -97,6 +97,9 @@ public class DragSourceExampleViewController: UIViewController {
let tossable = Tossable(destination: Destination(runtime.get(circle)), system: pop)
runtime.add(tossable, to: square)
runtime.add(Tap(), to: tossable)
runtime.add(Spring(to: tossable.destination.asProperty(), threshold: 1, system: coreAnimation), to: square2)

let spring = Spring<CGPoint>(threshold: 1, system: coreAnimation)
runtime.add(tossable.destination.asStream(), to: spring.destination)
runtime.add(spring, to: square2)
}
}
13 changes: 10 additions & 3 deletions examples/InteractivePushBackTransitionExample.swift
Expand Up @@ -77,17 +77,18 @@ private class PushBackTransitionDirector: TransitionDirector {
switch gestureRecognizer {
case let pan as UIPanGestureRecognizer:
let gesture = runtime.get(pan)

let dragStream = gesture.translated(from: foreLayer.position).y().min(foreLayer.layer.bounds.height / 2)
movement.compose { $0.toggled(with: dragStream) }
runtime.add(dragStream, to: foreLayer.positionY)

let scaleStream = dragStream.mapRange(rangeStart:movement.backwardDestination,
rangeEnd:movement.forwardDestination,
destinationStart:scale.backwardDestination,
destinationEnd:scale.forwardDestination)
scale.compose { $0.toggled(with: scaleStream) }
runtime.add(scaleStream, to: runtime.get(transition.back.view.layer).scale)

let velocityStream = gesture.velocityOnReleaseStream().y()
movement.add(initialVelocityStream: velocityStream)
runtime.add(velocityStream, to: movement.initialVelocity)

// TODO: Allow "whenWithin" to be a stream so that we can add additional logic for "have we
// passed the y threshold?"
Expand All @@ -96,13 +97,19 @@ private class PushBackTransitionDirector: TransitionDirector {
whenWithin: transition.direction.value,
whenAbove: .backward),
to: transition.direction)

runtime.add(gesture.atRest(), to: movement.enabled)
runtime.add(gesture.atRest(), to: scale.enabled)

default:
()
}
}

runtime.add(movement, to: foreLayer.positionY)
runtime.add(scale, to: runtime.get(transition.back.view.layer).scale)

transition.terminateWhenAllAtRest([movement.state.asStream(), scale.state.asStream()])
}

private func spring(back: CGFloat, fore: CGFloat, threshold: CGFloat, transition: Transition) -> TransitionSpring<CGFloat> {
Expand Down
24 changes: 16 additions & 8 deletions examples/ModalDialogExample.swift
Expand Up @@ -81,30 +81,33 @@ class ModalDialogTransitionDirector: SelfDismissingTransitionDirector {
let backPositionY = bounds.maxY + size.height * 3 / 4
let forePositionY = bounds.midY

let spring = TransitionSpring(back: backPositionY,
fore: forePositionY,
direction: transition.direction,
threshold: 1,
system: pop)
let mainThreadReactive: Bool
let system: SpringToStream<CGFloat>
if #available(iOS 9.0, *) {
mainThreadReactive = false
spring.system = coreAnimation
system = coreAnimation
} else {
mainThreadReactive = true
system = pop
}
let spring = TransitionSpring(back: backPositionY,
fore: forePositionY,
direction: transition.direction,
threshold: 1,
system: system)

let reactiveForeLayer = runtime.get(transition.fore.view.layer)

for gestureRecognizer in transition.gestureRecognizers {
switch gestureRecognizer {
case let pan as UIPanGestureRecognizer:
let gesture = runtime.get(pan)

let dragStream = gesture.translated(from: reactiveForeLayer.position).y()
spring.compose { $0.toggled(with: dragStream) }
runtime.add(dragStream, to: reactiveForeLayer.positionY)

let velocityStream = gesture.velocityOnReleaseStream().y()
spring.add(initialVelocityStream: velocityStream)
runtime.add(velocityStream, to: spring.initialVelocity)

let centerY = reactiveForeLayer.layer.bounds.height / 2.0
let withinStream = reactiveForeLayer.positionY.threshold(centerY,
Expand All @@ -116,6 +119,9 @@ class ModalDialogTransitionDirector: SelfDismissingTransitionDirector {
whenWithin: withinStream,
whenAbove: .backward),
to: transition.direction)

runtime.add(gesture.atRest(), to: spring.enabled)

default:
()
}
Expand All @@ -131,6 +137,8 @@ class ModalDialogTransitionDirector: SelfDismissingTransitionDirector {
destinationEnd: 0)
runtime.add(rotation, to: reactiveForeLayer.rotation)
}

transition.terminateWhenAllAtRest([spring.state.asStream()])
}

static func willPresent(fore: UIViewController, dismisser: ViewControllerDismisser) {
Expand Down
18 changes: 10 additions & 8 deletions examples/PushBackTransitionExample.swift
Expand Up @@ -66,14 +66,16 @@ private class PushBackTransitionDirector: TransitionDirector {
required init() {}

func willBeginTransition(_ transition: Transition, runtime: MotionRuntime) {
runtime.add(spring(back: transition.containerView().bounds.height + transition.fore.view.layer.bounds.height / 2,
fore: transition.containerView().bounds.midY,
threshold: 1,
transition: transition),
to: runtime.get(transition.fore.view.layer).positionY)

runtime.add(spring(back: 1, fore: 0.95, threshold: 0.005, transition: transition),
to: runtime.get(transition.back.view.layer).scale)
let position = spring(back: transition.containerView().bounds.height + transition.fore.view.layer.bounds.height / 2,
fore: transition.containerView().bounds.midY,
threshold: 1,
transition: transition)
let scale = spring(back: 1, fore: 0.95, threshold: 0.005, transition: transition)

runtime.add(position, to: runtime.get(transition.fore.view.layer).positionY)
runtime.add(scale, to: runtime.get(transition.back.view.layer).scale)

transition.terminateWhenAllAtRest([position.state.asStream(), scale.state.asStream()])
}

private func spring(back: CGFloat, fore: CGFloat, threshold: CGFloat, transition: Transition) -> TransitionSpring<CGFloat> {
Expand Down
5 changes: 4 additions & 1 deletion examples/StickerPickerExample.swift
Expand Up @@ -53,7 +53,8 @@ public class StickerPickerExampleViewController: UIViewController, StickerListVi
view.addSubview(imageView)

imageView.layer.transform = CATransform3DMakeScale(1.5, 1.5, 1)
let spring = Spring(to: CGFloat(1), threshold: 1, system: coreAnimation)
let spring = Spring<CGFloat>(threshold: 1, system: coreAnimation)
spring.destination.value = 1
runtime.add(spring, to: runtime.get(imageView.layer).scale)

runtime.add(DirectlyManipulable(targetView: imageView), to: imageView)
Expand Down Expand Up @@ -206,5 +207,7 @@ private class ModalTransitionDirector: TransitionDirector {
threshold: 0.01,
system: coreAnimation)
runtime.add(spring, to: runtime.get(transition.fore.view.layer).opacity)

transition.terminateWhenAllAtRest([spring.state.asStream()])
}
}
16 changes: 9 additions & 7 deletions examples/SwipeExample.swift
Expand Up @@ -73,14 +73,15 @@ class TossableStackedCard: ViewInteraction {
runtime.add(gestureEnabledStream, to: drag.isEnabled)
runtime.add(gestureEnabledStream, to: reactiveView.isUserInteractionEnabled)

let attachment = Spring(to: destination,
initialVelocity: drag.velocityOnReleaseStream(in: view).x(),
threshold: 1,
system: pop)
let attachment = Spring<CGFloat>(threshold: 1, system: pop)
runtime.add(drag.velocityOnReleaseStream(in: view).x(), to: attachment.initialVelocity)
runtime.add(destination.asStream(), to: attachment.destination)
runtime.add(reactiveView.centerX.asStream(), to: attachment.initialValue)

let draggable = drag.translated(from: reactiveView.center, in: relativeView).x()
runtime.add(attachment.stream(withInitialValue: reactiveView.centerX).toggled(with: draggable),
to: reactiveView.centerX)
runtime.add(draggable, to: reactiveView.centerX)
runtime.add(drag.atRest(), to: attachment.enabled)
runtime.add(attachment, to: reactiveView.centerX)

let radians = CGFloat(M_PI / 180.0 * 15.0)
let rotationStream =
Expand All @@ -101,7 +102,8 @@ class TossableStackedCard: ViewInteraction {
.max(1)
.subtracted(from: 1)
.scaled(by: rotation)
runtime.add(nextRotationStream.toggled(with: rotationStream), to: reactiveLayer.rotation)
runtime.add(nextRotationStream.valve(openWhenTrue: drag.atRest()), to: reactiveLayer.rotation)
runtime.add(rotationStream.valve(openWhenTrue: drag.active()), to: reactiveLayer.rotation)
} else {
runtime.add(rotationStream, to: reactiveLayer.rotation)
}
Expand Down
60 changes: 25 additions & 35 deletions src/interactions/Spring.swift
Expand Up @@ -15,38 +15,30 @@
*/

import Foundation
import IndefiniteObservable

/**
A Spring can pull a value from an initial position to a destination using a physical simulation.
This class defines the expected shape of a Spring for use in creating a Spring source.
*/
public class Spring<T: Zeroable>: ViewInteraction, PropertyInteraction {

/** Creates a spring with the provided properties and an initial velocity of zero. */
public convenience init<O: MotionObservableConvertible>(to destination: O, threshold: CGFloat, system: @escaping SpringToStream<T>) where O.T == T {
let initialVelocity = createProperty(withInitialValue: T.zero() as! T)
self.init(to: destination, initialVelocity: initialVelocity, threshold: threshold, system: system)
}

public class Spring<T: Zeroable>: PropertyInteraction, ViewInteraction {
/** Creates a spring with the provided properties and an initial velocity. */
public init<O1: MotionObservableConvertible, O2: MotionObservableConvertible>(to destination: O1,
initialVelocity: O2,
threshold: CGFloat,
system: @escaping SpringToStream<T>) where O1.T == T, O2.T == T {
self.destination = destination.asStream()
self.initialVelocity = initialVelocity.asStream()
self.system = system

public init(threshold: CGFloat, system: @escaping SpringToStream<T>) {
self.threshold = createProperty(withInitialValue: threshold)
self.system = system
}

/** The destination value of the spring represented as a property. */
public let destination: MotionObservable<T>
public let enabled = createProperty(withInitialValue: true)

public let state = createProperty(withInitialValue: MotionState.atRest)

public let initialValue: ReactiveProperty<T> = createProperty()

/** The initial velocity of the spring represented as a stream. */
public private(set) var initialVelocity: MotionObservable<T>
public let initialVelocity: ReactiveProperty<T> = createProperty()

/** The destination value of the spring represented as a property. */
public let destination: ReactiveProperty<T> = createProperty()

/** The tension configuration of the spring represented as a property. */
public let tension = createProperty(withInitialValue: defaultSpringTension)
Expand All @@ -69,31 +61,29 @@ public class Spring<T: Zeroable>: ViewInteraction, PropertyInteraction {
/** The value used when determining completion of the spring simulation. */
public let threshold: ReactiveProperty<CGFloat>

public var system: SpringToStream<T>

/** The stream of values generated by this spring. */
public func stream<O: MotionObservableConvertible>(withInitialValue initialValue: O) -> MotionObservable<T> where O.T == T {
return compositions.reduce(system(self, initialValue.asStream())) { $1($0) }
}

public func compose(stream: @escaping (MotionObservable<T>) -> MotionObservable<T>) {
compositions.append(stream)
}
private var compositions: [(MotionObservable<T>) -> MotionObservable<T>] = []
fileprivate var stream: MotionObservable<T>?
fileprivate let system: SpringToStream<T>

public func add(to reactiveView: ReactiveUIView, withRuntime runtime: MotionRuntime) {
if let castedSelf = self as? Spring<CGPoint> {
let position = reactiveView.reactiveLayer.position
runtime.add(castedSelf.stream(withInitialValue: position), to: position)
runtime.add(position.asStream(), to: castedSelf.initialValue)
runtime.add(castedSelf.asStream(), to: position)
}
}

public func add(to property: ReactiveProperty<T>, withRuntime runtime: MotionRuntime) {
runtime.add(stream(withInitialValue: property), to: property)
runtime.add(property.asStream(), to: initialValue)
runtime.add(asStream(), to: property)
}
}

public func add(initialVelocityStream stream: MotionObservable<T>) {
initialVelocity = initialVelocity.merge(with: stream)
extension Spring: MotionObservableConvertible {
public func asStream() -> MotionObservable<T> {
if stream == nil {
stream = system(self).multicast()
}
return stream!
}
}

Expand Down
18 changes: 10 additions & 8 deletions src/interactions/Tossable.swift
Expand Up @@ -46,15 +46,17 @@ public class Tossable: ViewInteraction {
public func add(to reactiveView: ReactiveUIView, withRuntime runtime: MotionRuntime) {
let position = reactiveView.reactiveLayer.position
let relativeView = draggable.relativeView ?? runtime.containerView

let gesture = runtime.get(draggable.gestureRecognizer)
let spring = Spring(to: destination,
initialVelocity: gesture.velocityOnReleaseStream(in: relativeView),
threshold: 1,
system: system)
let dragStream = gesture.translated(from: reactiveView.center, in: relativeView)
let tossStream = spring.stream(withInitialValue: reactiveView.center).toggled(with: dragStream)
runtime.add(tossStream, to: reactiveView.center)

runtime.add(gesture.translated(from: position, in: relativeView), to: position)

let spring = Spring(threshold: 1, system: system)
runtime.add(destination.asStream(), to: spring.destination)
runtime.add(position.asStream(), to: spring.initialValue)
runtime.add(gesture.velocityOnReleaseStream(in: relativeView), to: spring.initialVelocity)
runtime.add(spring, to: position)

runtime.add(gesture.atRest(), to: spring.enabled)
}
}

Expand Down
11 changes: 6 additions & 5 deletions src/interactions/TransitionSpring.swift
Expand Up @@ -41,19 +41,20 @@ public class TransitionSpring<T: Zeroable>: Spring<T>, TransitionInteraction {
self.forwardDestination = forwardDestination
self._initialValue = direction == .forward ? backwardDestination : forwardDestination

let destinationStream = direction.stream.destinations(back: backwardDestination,
fore: forwardDestination)
let initialVelocity = createProperty(withInitialValue: T.zero() as! T)
super.init(to: destinationStream, initialVelocity: initialVelocity, threshold: threshold, system: system)
self.toggledDestination = direction.stream.destinations(back: backwardDestination,
fore: forwardDestination)
super.init(threshold: threshold, system: system)
}

public override func add(to property: ReactiveProperty<T>, withRuntime runtime: MotionRuntime) {
runtime.add(stream(withInitialValue: property), to: property)
runtime.add(toggledDestination, to: destination)
super.add(to: property, withRuntime: runtime)
}

public func initialValue() -> T {
return _initialValue
}

private let _initialValue: T
private let toggledDestination: MotionObservable<T>
}
2 changes: 1 addition & 1 deletion src/sources/SpringToStream.swift
Expand Up @@ -19,4 +19,4 @@ import UIKit
/**
A spring-to-stream function creates a MotionObservable from a Spring and initial value stream.
*/
public typealias SpringToStream<T: Zeroable> = (Spring<T>, MotionObservable<T>) -> MotionObservable<T>
public typealias SpringToStream<T: Zeroable> = (Spring<T>) -> MotionObservable<T>

0 comments on commit 41fee56

Please sign in to comment.