diff --git a/.jazzy.yaml b/.jazzy.yaml
index c89f7d1..f7f62c3 100644
--- a/.jazzy.yaml
+++ b/.jazzy.yaml
@@ -1,5 +1,5 @@
module: MaterialMotion
-module_version: 1.0.0
+module_version: 1.1.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.0.0
+github_file_prefix: https://github.com/material-motion/material-motion-swift/tree/v1.1.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6daf7ef..9923f79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,143 @@
+# 1.1.0
+
+This is our first minor release. It includes two new interactions and improvements to APIs for the common cases.
+
+
+
+## Behavioral changes
+
+- TransitionSpring's configuration now defaults to Core Animation's default values. If you prefer to continue using the original Spring defaults you can use the following snippet:
+
+```swift
+spring.tension.value = defaultSpringTension
+spring.friction.value = defaultSpringFriction
+spring.mass.value = defaultSpringMass
+spring.suggestedDuration.value = 0
+```
+
+## New deprecations
+
+- Tossable's `init(system:draggable:)` is deprecated in favor of `init(spring:draggable:)`.
+
+## New features
+
+New interactions: `ChangeDirection` and `SlopRegion`.
+
+Gesturable interactions can now be initialized with a sequence of gesture recognizers. This makes it easier to create gesturable interactions in transitions where the gesture recognizers are provided as a set.
+
+Spring's system now defaults to Core Animation.
+
+There is a new API for getting a gesture recognizer delegate that's able to coordinate a "drag to dismiss" transition with a vertical scroll view.
+
+```swift
+let pan = UIPanGestureRecognizer()
+pan.delegate = transitionController.dismisser.topEdgeDismisserDelegate(for: scrollView)
+```
+
+## Source changes
+
+* [Store interactions before adding them so that order is maintained when interactions add other interactions.](https://github.com/material-motion/material-motion-swift/commit/5d0af5fa77b913c0706837d1f0bbf3e14ce9f1ad) (Jeff Verkoeyen)
+* [Avoid over-completion of chained property animations.](https://github.com/material-motion/material-motion-swift/commit/51ee058ddc3a15164f4a3c22c522cf3e6fd280c9) (Jeff Verkoeyen)
+* [Fix bug causing properties-chained-to-properties to not animate correctly.](https://github.com/material-motion/material-motion-swift/commit/9e810bbf0ddce9764c76ea44dc223ccee4fca0b7) (Jeff Verkoeyen)
+* [Add SlopRegion interaction.](https://github.com/material-motion/material-motion-swift/commit/2655adc561181c0679f52df2ca23646abf7876cb) (Jeff Verkoeyen)
+* [Add topEdgeDismisserDelegate API to ViewControllerDismisser.](https://github.com/material-motion/material-motion-swift/commit/ad32ba9e7200b59e799d57c0c964b19be55089c3) (Jeff Verkoeyen)
+* [Add Gesturable convenience initializer for extracting the first gesture recognizer from a sequence of gesture recognizers.](https://github.com/material-motion/material-motion-swift/commit/c4e0b3d0bf5c2b53e0046c08b63f0592b99b4b46) (Jeff Verkoeyen)
+* [Rename ChangeDirectionOnRelease(of:) to ChangeDirection(withVelocityOf:)](https://github.com/material-motion/material-motion-swift/commit/c999a3a5134331e79f3cce74d2be222780155054) (Jeff Verkoeyen)
+* [Fix crashing bug when connecting properties to one another.](https://github.com/material-motion/material-motion-swift/commit/840d97cd68321dd241e157ed9527cdc83ed73489) (Jeff Verkoeyen)
+* [Add ChangeDirectionOnRelease interaction.](https://github.com/material-motion/material-motion-swift/commit/bdee45533c68f2c98e7b543acf9e3589a783eb46) (Jeff Verkoeyen)
+* [Make TransitionSpring and Spring T type conform to Subtractable so that coreAnimation can be set as the default system.](https://github.com/material-motion/material-motion-swift/commit/419327972893b92a4317e903ad933550d096a76f) (Jeff Verkoeyen)
+* [TransitionSpring configuration now defaults to Core Animation configuration defaults.](https://github.com/material-motion/material-motion-swift/commit/295d64556048429a5ec6d7859a0ce8922b84a4fd) (Jeff Verkoeyen)
+
+## API changes
+
+Auto-generated by running:
+
+ apidiff origin/stable release-candidate swift MaterialMotion.xcworkspace MaterialMotion
+
+### New global constants
+
+*new* global var: `defaultTransitionSpringFriction`
+
+*new* global var: `defaultTransitionSpringSuggestedDuration`
+
+*new* global var: `defaultTransitionSpringTension`
+
+*new* global var: `defaultTransitionSpringMass`
+
+### New interactions
+
+*new* class: `ChangeDirection`
+
+*new* class: `SlopRegion`
+
+### Modified interactions
+
+#### Gesturable
+
+Affects `Draggable`, `Rotatable`, and `Scalable`.
+
+*new* method: `init(withFirstGestureIn:)` in `Gesturable`
+
+#### Spring
+
+| Type of change: | Declaration |
+|---|---|
+| From: | `public class Spring : Interaction, Togglable, Stateful where T : Zeroable` |
+| To: | `public class Spring : Interaction, Togglable, Stateful where T : Subtractable, T : Zeroable` |
+
+*modified* method: `init(threshold:system:)` in `Spring`
+
+| Type of change: | Declaration |
+|---|---|
+| From: | `public init(threshold: CGFloat, system: @escaping SpringToStream)` |
+| To: | `public init(threshold: CGFloat = 1, system: @escaping SpringToStream = coreAnimation)` |
+
+#### Tossable
+
+*modified* method: `init(spring:draggable:)` in `Tossable`
+
+| Type of change: | Declaration |
+|---|---|
+| From: | `public init(spring: Spring, draggable: Draggable = Draggable())` |
+| To: | `public init(spring: Spring = Spring(), draggable: Draggable = Draggable())` |
+
+*deprecated* method: `init(system:draggable:)` in `Tossable`. Use `init(spring:draggable:)` instead.
+
+#### TransitionSpring
+
+*modified* class: `TransitionSpring`
+
+| Type of change: | Declaration |
+|---|---|
+| From: | `public final class TransitionSpring : Spring where T : Zeroable` |
+| To: | `public final class TransitionSpring : Spring where T : Subtractable, T : Zeroable` |
+
+*modified* method: `init(back:fore:direction:threshold:system:)` in `TransitionSpring`
+
+| Type of change: | Declaration |
+|---|---|
+| From: | `public init(back backwardDestination: T, fore forwardDestination: T, direction: ReactiveProperty, threshold: CGFloat, system: @escaping SpringToStream)` |
+| To: | `public init(back backwardDestination: T, fore forwardDestination: T, direction: ReactiveProperty, threshold: CGFloat = default, system: @escaping SpringToStream = default)` |
+
+### Transitions
+
+#### ViewControllerDismisser
+
+*new* method: `topEdgeDismisserDelegate(for:)` in `ViewControllerDismisser`
+
+### Stream changes
+
+*new* var: `onCompletion` in `CoreAnimationChannelAdd`
+
+*removed* var: `onCompletion` in `CoreAnimationChannelAdd`
+
+## Non-source changes
+
+* [Modify the PushBackTransition example to use connected properties instead of multiple springs.](https://github.com/material-motion/material-motion-swift/commit/71f06c23ec9ef8f460ad48de6f6af540f1eec4c9) (Jeff Verkoeyen)
+* [Simplify the interactive push back transition example.](https://github.com/material-motion/material-motion-swift/commit/b8620649aae2eadf93ec66c82b164759a067931d) (Jeff Verkoeyen)
+* [Add syntax highlighting languages to the README.](https://github.com/material-motion/material-motion-swift/commit/486cff7dc497a66c5479f8117a599cf778fa575a) (Jeff Verkoeyen)
+* [Add example Podfile to the README.](https://github.com/material-motion/material-motion-swift/commit/687e6cc01dc82bd114a5f4f913cee54dc5071806) (Jeff Verkoeyen)
+
# 1.0.0
Initial stable release of Material Motion. Includes:
diff --git a/MaterialMotion.podspec b/MaterialMotion.podspec
index 972a60b..d4c168b 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.0.0"
+ s.version = "1.1.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 3652598..9b6bb85 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -3,7 +3,7 @@ PODS:
- IndefiniteObservable (3.1.0):
- IndefiniteObservable/lib (= 3.1.0)
- IndefiniteObservable/lib (3.1.0)
- - MaterialMotion (1.0.0):
+ - MaterialMotion (1.1.0):
- IndefiniteObservable (~> 3.0)
DEPENDENCIES:
@@ -17,7 +17,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
CatalogByConvention: be55c2263132e4f9f59299ac8a528ee8715b3275
IndefiniteObservable: 2789d61f487d8d37fa2b9c3153cc44d4447ff744
- MaterialMotion: 967eed88a2f4add305ea8768bbc49283283c1e83
+ MaterialMotion: 6ee4d44d39b074686d603c26c20a5816afdb50cd
PODFILE CHECKSUM: f503265a0d60526a0d28c96dd4bdcfb40fb562fc
diff --git a/README.md b/README.md
index 2987105..2856328 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,12 @@ arcMove.from.value = <#from#>
arcMove.to.value = <#to#>
runtime.add(arcMove, to: <#view#>)
+
+
+
ChangeDirection
+
runtime.add(ChangeDirection(withVelocityOf: gesture),
+ to: <#view#>)
+
DirectlyManipulable
@@ -91,17 +97,38 @@ runtime.add(tossable, to: <#view#>)
Add `MaterialMotion` to your `Podfile`:
- pod 'MaterialMotion'
+```ruby
+pod 'MaterialMotion'
+```
+
+You will need to add `use_frameworks!` to your Podfile in order use Material Motion in your swift
+app.
+
+A simple Podfile might look like so:
+
+```ruby
+project 'MyApp/MyApp.xcodeproj'
+
+use_frameworks!
+
+target 'MyApp' do
+ pod 'MaterialMotion'
+end
+```
Then run the following command:
- pod install
+```bash
+pod install
+```
### Usage
Import the framework:
- import MaterialMotion
+```swift
+import MaterialMotion
+```
You will now have access to all of the APIs.
@@ -110,10 +137,12 @@ You will now have access to all of the APIs.
Check out a local copy of the repo to access the Catalog application by running the following
commands:
- git clone https://github.com/material-motion/material-motion-swift.git
- cd material-motion-swift
- pod install
- open MaterialMotion.xcworkspace
+```bash
+git clone https://github.com/material-motion/material-motion-swift.git
+cd material-motion-swift
+pod install
+open MaterialMotion.xcworkspace
+```
## Case studies
@@ -168,6 +197,16 @@ Makes use of: `Tossable` and `TransitionSpring`.
[View the source](examples/ModalDialogExample.swift).
+### Pull down to dismiss
+
+
+
+A modal scroll view controller that can be dismissed with a drag gesture.
+
+Makes use of: `Tossable` and `TransitionSpring`.
+
+[View the source](examples/InteractivePushBackTransitionExample.swift).
+
### Sticker picker
diff --git a/assets/changedirection.gif b/assets/changedirection.gif
new file mode 100644
index 0000000..0970b85
Binary files /dev/null and b/assets/changedirection.gif differ
diff --git a/assets/pulldowntodismiss.gif b/assets/pulldowntodismiss.gif
new file mode 100644
index 0000000..7711bd1
Binary files /dev/null and b/assets/pulldowntodismiss.gif differ
diff --git a/examples/ChangeDirectionOnReleaseExample.swift b/examples/ChangeDirectionOnReleaseExample.swift
new file mode 100644
index 0000000..259d092
--- /dev/null
+++ b/examples/ChangeDirectionOnReleaseExample.swift
@@ -0,0 +1,56 @@
+/*
+ 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 MaterialMotion
+
+class ChangeDirectionOnReleaseExampleViewController: ExampleViewController {
+
+ var runtime: MotionRuntime!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ let targetView = center(createExampleSquareView(), within: view)
+ targetView.layer.borderColor = targetView.backgroundColor?.cgColor
+ targetView.layer.borderWidth = 1
+ targetView.backgroundColor = nil
+ view.addSubview(targetView)
+
+ let exampleView = center(createExampleView(), within: view)
+ view.addSubview(exampleView)
+
+ runtime = MotionRuntime(containerView: view)
+
+ let direction = createProperty(withInitialValue: TransitionDirection.backward)
+
+ let positionSpring = TransitionSpring(back: CGPoint(x: view.bounds.midX, y: view.bounds.height * 4 / 10),
+ fore: CGPoint(x: view.bounds.midX, y: view.bounds.height * 6 / 10),
+ direction: direction)
+ let tossable = Tossable(spring: positionSpring)
+ runtime.add(ChangeDirection(withVelocityOf: tossable.draggable.nextGestureRecognizer,
+ whenNegative: .backward,
+ whenPositive: .forward),
+ to: direction)
+ runtime.add(tossable, to: exampleView)
+ runtime.add(positionSpring, to: runtime.get(targetView.layer).position)
+ }
+
+ override func exampleInformation() -> ExampleInfo {
+ return .init(title: type(of: self).catalogBreadcrumbs().last!,
+ instructions: "Toss the view to change its position.")
+ }
+}
diff --git a/examples/ContextualTransitionExample.swift b/examples/ContextualTransitionExample.swift
index 2cc9f81..0767da9 100644
--- a/examples/ContextualTransitionExample.swift
+++ b/examples/ContextualTransitionExample.swift
@@ -269,62 +269,27 @@ private class PushBackTransition: Transition {
foreImageView.bounds.height / imageSize.height)
let fitSize = CGSize(width: fitScale * imageSize.width, height: fitScale * imageSize.height)
- let firstPan = ctx.gestureRecognizers.first { $0 is UIPanGestureRecognizer }
- let draggable: Draggable
- if let firstPan = firstPan as? UIPanGestureRecognizer {
- draggable = Draggable(.withExistingRecognizer(firstPan))
- } else {
- draggable = Draggable()
- }
+ let draggable = Draggable(withFirstGestureIn: ctx.gestureRecognizers)
+
+ runtime.add(SlopRegion(withTranslationOf: draggable.nextGestureRecognizer, size: 100), to: ctx.direction)
+ runtime.add(ChangeDirection(withVelocityOf: draggable.nextGestureRecognizer), to: ctx.direction)
- let gesture = runtime.get(draggable.nextGestureRecognizer)
- runtime.connect(gesture
- .translation(in: runtime.containerView)
- .y()
- .slop(size: 100)
- .rewrite([.onExit: .backward, .onReturn: .forward]),
- to: ctx.direction)
- runtime.connect(gesture
- .velocityOnReleaseStream()
- .y()
- .thresholdRange(min: -100, max: 100)
- .rewrite([.below: .backward, .above: .backward]),
- to: ctx.direction)
-
- let movement = spring(back: contextView, fore: foreImageView, ctx: ctx)
+ let backPosition = contextView.superview!.convert(contextView.layer.position, to: ctx.containerView())
+ let forePosition = foreImageView.superview!.convert(foreImageView.layer.position, to: ctx.containerView())
+ let movement = TransitionSpring(back: backPosition, fore: forePosition, direction: ctx.direction)
let tossable = Tossable(spring: movement, draggable: draggable)
runtime.add(tossable, to: replicaView)
- let size = spring(back: contextView.bounds.size, fore: fitSize, threshold: 1, ctx: ctx)
+ 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)
- let opacity = spring(back: CGFloat(0), fore: CGFloat(1), threshold: 0.01, ctx: ctx)
+ let opacity = TransitionSpring(back: 0, fore: 1, direction: ctx.direction)
runtime.add(opacity, to: runtime.get(ctx.fore.view.layer).opacity)
runtime.add(Hidden(), to: foreImageView)
- return [tossable.spring, gesture, size, opacity]
- }
-
- private func spring(back: T, fore: T, threshold: CGFloat, ctx: TransitionContext) -> TransitionSpring where T: Subtractable, T: Zeroable, T: Equatable {
- let spring = TransitionSpring(back: back, fore: fore, direction: ctx.direction, threshold: threshold, system: coreAnimation)
- spring.friction.value = 500
- spring.tension.value = 1000
- spring.mass.value = 3
- spring.suggestedDuration.value = 0.5
- return spring
- }
-
- private func spring(back: UIView, fore: UIView, ctx: TransitionContext) -> TransitionSpring {
- let backPosition = back.superview!.convert(back.layer.position, to: ctx.containerView())
- let forePosition = fore.superview!.convert(fore.layer.position, to: ctx.containerView())
- let spring = TransitionSpring(back: backPosition, fore: forePosition, direction: ctx.direction, threshold: 1, system: coreAnimation)
- spring.friction.value = 500
- spring.tension.value = 1000
- spring.mass.value = 3
- spring.suggestedDuration.value = 0.5
- return spring
+ return [tossable, size, opacity]
}
}
diff --git a/examples/InteractivePushBackTransitionExample.swift b/examples/InteractivePushBackTransitionExample.swift
index a77b73c..4ffaa6b 100644
--- a/examples/InteractivePushBackTransitionExample.swift
+++ b/examples/InteractivePushBackTransitionExample.swift
@@ -36,7 +36,7 @@ class InteractivePushBackTransitionExampleViewController: ExampleViewController
}
}
-private class ModalViewController: UIViewController {
+private class ModalViewController: UIViewController, UIGestureRecognizerDelegate {
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
@@ -48,13 +48,20 @@ private class ModalViewController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
+ var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .primaryColor
+ scrollView = UIScrollView(frame: view.bounds)
+ scrollView.contentSize = .init(width: view.bounds.width, height: view.bounds.height * 10)
+ view.addSubview(scrollView)
+
let pan = UIPanGestureRecognizer()
+ pan.delegate = transitionController.dismisser.topEdgeDismisserDelegate(for: scrollView)
transitionController.dismisser.dismissWhenGestureRecognizerBegins(pan)
+ scrollView.panGestureRecognizer.require(toFail: pan)
view.addGestureRecognizer(pan)
}
}
@@ -64,63 +71,31 @@ private class PushBackTransition: Transition {
required init() {}
func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] {
- let firstPan = ctx.gestureRecognizers.first { $0 is UIPanGestureRecognizer }
- let draggable: Draggable
- if let firstPan = firstPan as? UIPanGestureRecognizer {
- draggable = Draggable(.withExistingRecognizer(firstPan))
- } else {
- draggable = Draggable()
- }
-
- let gesture = runtime.get(draggable.nextGestureRecognizer)
- runtime.connect(gesture
- .velocityOnReleaseStream()
- .y()
- .thresholdRange(min: -100, max: 100)
- .rewrite([.below: .forward,
- .within: ctx.direction.value,
- .above: .backward]),
+ let draggable = Draggable(withFirstGestureIn: ctx.gestureRecognizers)
+
+ runtime.add(ChangeDirection(withVelocityOf: draggable.nextGestureRecognizer, whenNegative: .forward),
to: ctx.direction)
let bounds = ctx.containerView().bounds
let backPosition = CGPoint(x: bounds.midX, y: bounds.maxY + ctx.fore.view.bounds.height / 2)
let forePosition = CGPoint(x: bounds.midX, y: bounds.midY)
- let movement = spring(back: backPosition,
- fore: forePosition,
- threshold: 1,
- ctx: ctx)
- let scaleSpring = spring(back: CGFloat(1), fore: CGFloat(0.95), threshold: 0.005, ctx: ctx)
+ let movement = TransitionSpring(back: backPosition,
+ fore: forePosition,
+ direction: ctx.direction)
let scale = runtime.get(ctx.back.view.layer).scale
- runtime.connect(runtime.get(ctx.fore.view.layer).position.y()
- // The position's final value gets written to by Core Animation when the gesture ends and the
- // movement spring engages. Because we're connecting position to the scale here, this would
- // also cause scale to jump to its destination as well (without animating, unfortunately).
- // To ensure that we don't receive this information, we valve the stream based on the gesture
- // activity and ensure that we register this valve *before* committing Tossable to the
- // runtime.
- .valve(openWhenTrue: gesture.active())
- .rewriteRange(start: movement.backwardDestination.y,
- end: movement.forwardDestination.y,
- destinationStart: scaleSpring.backwardDestination,
- destinationEnd: scaleSpring.forwardDestination),
- to: scale)
let tossable = Tossable(spring: movement, draggable: draggable)
- runtime.add(tossable, to: ctx.fore.view) { $0.xLocked(to: ctx.fore.view.layer.position.x) }
- runtime.toggle(scaleSpring, inReactionTo: draggable)
- runtime.add(scaleSpring, to: scale)
+ runtime.connect(runtime.get(ctx.fore.view.layer).position.y()
+ .rewriteRange(start: movement.backwardDestination.y,
+ end: movement.forwardDestination.y,
+ destinationStart: 1,
+ destinationEnd: 0.95),
+ to: scale)
- return [tossable.spring, scaleSpring, gesture]
- }
+ runtime.add(tossable, to: ctx.fore.view) { $0.xLocked(to: bounds.midX) }
- private func spring(back: T, fore: T, threshold: CGFloat, ctx: TransitionContext) -> TransitionSpring where T: Subtractable, T: Zeroable, T: Equatable {
- let spring = TransitionSpring(back: back, fore: fore, direction: ctx.direction, threshold: threshold, system: coreAnimation)
- spring.friction.value = 500
- spring.tension.value = 1000
- spring.mass.value = 3
- spring.suggestedDuration.value = 0.5
- return spring
+ return [tossable]
}
}
diff --git a/examples/ModalDialogExample.swift b/examples/ModalDialogExample.swift
index 6f972af..ca44606 100644
--- a/examples/ModalDialogExample.swift
+++ b/examples/ModalDialogExample.swift
@@ -84,36 +84,28 @@ class ModalDialogTransition: SelfDismissingTransition {
let backPosition = CGPoint(x: bounds.midX, y: bounds.maxY + size.height * 3 / 4)
let forePosition = CGPoint(x: bounds.midX, y: bounds.midY)
- let firstPan = ctx.gestureRecognizers.first { $0 is UIPanGestureRecognizer }
- let draggable: Draggable
- if let firstPan = firstPan as? UIPanGestureRecognizer {
- draggable = Draggable(.withExistingRecognizer(firstPan))
- } else {
- draggable = Draggable()
- }
-
let reactiveForeLayer = runtime.get(ctx.fore.view.layer)
let position = reactiveForeLayer.position
+ 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)
- // If one of rewrite's target values is a stream, then all the target values must be
- // streams.
- .rewrite([.below: createProperty(withInitialValue: .forward).asStream(),
- .within: position.y().threshold(centerY).rewrite([.below: .forward,
- .above: .backward]),
- .above: createProperty(withInitialValue: .backward).asStream()]),
+ .rewrite([.within: position.y().threshold(centerY).rewrite([.below: .forward,
+ .above: .backward])]),
to: ctx.direction)
let movement = TransitionSpring(back: backPosition,
fore: forePosition,
- direction: ctx.direction,
- threshold: 1,
- system: coreAnimation)
+ direction: ctx.direction)
let tossable = Tossable(spring: movement, draggable: draggable)
runtime.add(tossable, to: ctx.fore.view)
diff --git a/examples/PushBackTransitionExample.swift b/examples/PushBackTransitionExample.swift
index 6cbc6ea..27cae79 100644
--- a/examples/PushBackTransitionExample.swift
+++ b/examples/PushBackTransitionExample.swift
@@ -66,24 +66,22 @@ private class PushBackTransition: Transition {
required init() {}
func willBeginTransition(withContext ctx: TransitionContext, runtime: MotionRuntime) -> [Stateful] {
- let position = spring(back: ctx.containerView().bounds.height + ctx.fore.view.layer.bounds.height / 2,
- fore: ctx.containerView().bounds.midY,
- threshold: 1,
- ctx: ctx)
- let scale = spring(back: 1, fore: 0.95, threshold: 0.005, ctx: ctx)
+ let bounds = ctx.containerView().bounds
+ let backPosition = bounds.maxY + ctx.fore.view.bounds.height / 2
+ let forePosition = bounds.midY
- runtime.add(position, to: runtime.get(ctx.fore.view.layer).positionY)
- runtime.add(scale, to: runtime.get(ctx.back.view.layer).scale)
+ let movement = TransitionSpring(back: backPosition, fore: forePosition, direction: ctx.direction)
- return [position, scale]
- }
+ let yPosition = runtime.get(ctx.fore.view.layer).positionY
+
+ runtime.connect(yPosition.rewriteRange(start: movement.backwardDestination,
+ end: movement.forwardDestination,
+ destinationStart: CGFloat(1),
+ destinationEnd: CGFloat(0.95)),
+ to: runtime.get(ctx.back.view.layer).scale)
+
+ runtime.add(movement, to: yPosition)
- private func spring(back: CGFloat, fore: CGFloat, threshold: CGFloat, ctx: TransitionContext) -> TransitionSpring {
- let spring = TransitionSpring(back: back, fore: fore, direction: ctx.direction, threshold: threshold, system: coreAnimation)
- spring.friction.value = 500
- spring.tension.value = 1000
- spring.mass.value = 3
- spring.suggestedDuration.value = 0.5
- return spring
+ return [movement]
}
}
diff --git a/examples/StickerPickerExample.swift b/examples/StickerPickerExample.swift
index c34334d..895f679 100644
--- a/examples/StickerPickerExample.swift
+++ b/examples/StickerPickerExample.swift
@@ -37,7 +37,7 @@ class StickerPickerExampleViewController: ExampleViewController, StickerListView
view.addSubview(stickerView)
let direction = createProperty(withInitialValue: TransitionDirection.forward)
- let spring = TransitionSpring(back: CGFloat(1.5), fore: 1, direction: direction, threshold: 0.1, system: coreAnimation)
+ let spring = TransitionSpring(back: CGFloat(1.5), fore: 1, direction: direction)
runtime.add(spring, to: runtime.get(stickerView.layer).scale)
runtime.add(DirectlyManipulable(), to: stickerView)
@@ -201,11 +201,7 @@ private class ModalTransition: Transition {
ctx.fore.view.bounds = CGRect(origin: .zero, size: size)
}
- let spring = TransitionSpring(back: CGFloat(0),
- fore: CGFloat(1),
- direction: ctx.direction,
- threshold: 0.01,
- system: coreAnimation)
+ let spring = TransitionSpring(back: 0, fore: 1, direction: ctx.direction)
runtime.add(spring, to: runtime.get(ctx.fore.view.layer).opacity)
return [spring]
diff --git a/examples/apps/Catalog/Catalog/TableOfContents.swift b/examples/apps/Catalog/Catalog/TableOfContents.swift
index 32ff40c..517756b 100644
--- a/examples/apps/Catalog/Catalog/TableOfContents.swift
+++ b/examples/apps/Catalog/Catalog/TableOfContents.swift
@@ -62,6 +62,10 @@ extension ArcMoveExampleViewController {
class func catalogBreadcrumbs() -> [String] { return ["Interactions", "Arc move"] }
}
+extension ChangeDirectionOnReleaseExampleViewController {
+ class func catalogBreadcrumbs() -> [String] { return ["Interactions", "Change direction on release"] }
+}
+
extension DirectlyManipulableExampleViewController {
class func catalogBreadcrumbs() -> [String] { return ["Interactions", "Directly manipulable"] }
}
diff --git a/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj
index e06876b..60fe369 100644
--- a/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj
+++ b/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj
@@ -71,6 +71,7 @@
66DDFD161E71F90F00AA46B7 /* HowToUseReactiveConstraintsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DDFD151E71F90F00AA46B7 /* HowToUseReactiveConstraintsExample.swift */; };
66DDFD201E73281200AA46B7 /* HowToMakeACustomOperatorExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DDFD1F1E73281200AA46B7 /* HowToMakeACustomOperatorExample.swift */; };
66E6F2371E3FC791003158D3 /* PhotoAlbum.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 66E6F2361E3FC791003158D3 /* PhotoAlbum.xcassets */; };
+ 66F09E511E88B7EF00FC8D1B /* ChangeDirectionOnReleaseExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F09E501E88B7EF00FC8D1B /* ChangeDirectionOnReleaseExample.swift */; };
66F2C3C91E83245800DD9728 /* invertedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2C3C81E83245800DD9728 /* invertedTests.swift */; };
D3F169473691C7D44E72DEED /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D89BD1C45FD980DEF396AB66 /* Pods_UnitTests.framework */; };
/* End PBXBuildFile section */
@@ -157,6 +158,7 @@
66DDFD151E71F90F00AA46B7 /* HowToUseReactiveConstraintsExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HowToUseReactiveConstraintsExample.swift; path = ../../HowToUseReactiveConstraintsExample.swift; sourceTree = ""; };
66DDFD1F1E73281200AA46B7 /* HowToMakeACustomOperatorExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HowToMakeACustomOperatorExample.swift; path = ../../HowToMakeACustomOperatorExample.swift; sourceTree = ""; };
66E6F2361E3FC791003158D3 /* PhotoAlbum.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = PhotoAlbum.xcassets; sourceTree = SOURCE_ROOT; };
+ 66F09E501E88B7EF00FC8D1B /* ChangeDirectionOnReleaseExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ChangeDirectionOnReleaseExample.swift; path = ../../ChangeDirectionOnReleaseExample.swift; sourceTree = ""; };
66F2C3C81E83245800DD9728 /* invertedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = invertedTests.swift; sourceTree = ""; };
773711C342E83B1FF1F9C8BB /* Pods-MaterialMotionCatalog.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MaterialMotionCatalog.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-MaterialMotionCatalog/Pods-MaterialMotionCatalog.debug.xcconfig"; sourceTree = ""; };
800852A3E58C3B077D36E8DD /* Pods-MaterialMotionCatalog.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MaterialMotionCatalog.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-MaterialMotionCatalog/Pods-MaterialMotionCatalog.release.xcconfig"; sourceTree = ""; };
@@ -224,6 +226,7 @@
660DA31C1E7A07BC008F7401 /* SpringExample.swift */,
660DA3151E79F6A3008F7401 /* TossableExample.swift */,
660DA3201E7A106D008F7401 /* TweenExample.swift */,
+ 66F09E501E88B7EF00FC8D1B /* ChangeDirectionOnReleaseExample.swift */,
);
name = Interactions;
sourceTree = "";
@@ -618,6 +621,7 @@
files = (
667E945B1E71EF0E005CAC78 /* StickerPickerExample.swift in Sources */,
667E94551E71EF0E005CAC78 /* DirectlyManipulableExample.swift in Sources */,
+ 66F09E511E88B7EF00FC8D1B /* ChangeDirectionOnReleaseExample.swift in Sources */,
666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */,
660DA3171E79F6A3008F7401 /* TossableExample.swift in Sources */,
660DA30F1E79EA62008F7401 /* RotatableExample.swift in Sources */,
diff --git a/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to combine interactions.xcplaygroundpage/Contents.swift b/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to combine interactions.xcplaygroundpage/Contents.swift
index 8428eaa..0e37cac 100644
--- a/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to combine interactions.xcplaygroundpage/Contents.swift
+++ b/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to combine interactions.xcplaygroundpage/Contents.swift
@@ -14,7 +14,7 @@ let runtime = MotionRuntime(containerView: canvas)
//: ---
//:
//: First we create the interactions we know we'll need: Spring and Draggable.
-let spring = Spring(threshold: 1, system: coreAnimation)
+let spring = Spring()
let draggable = Draggable()
//: Inspecting Draggable's documentation reveals that it will affect the view's layer position (Option-Click Draggable), so let's make sure we use the same property for our spring:
diff --git a/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to create new interactions.xcplaygroundpage/Contents.swift b/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to create new interactions.xcplaygroundpage/Contents.swift
index a96fda4..7e3fcb6 100644
--- a/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to create new interactions.xcplaygroundpage/Contents.swift
+++ b/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to create new interactions.xcplaygroundpage/Contents.swift
@@ -22,7 +22,7 @@ final class MyTossable: Interaction {
let draggable: Draggable
let spring: Spring
- init(spring: Spring, draggable: Draggable = Draggable()) {
+ init(spring: Spring = Spring(), draggable: Draggable = Draggable()) {
self.spring = spring
self.draggable = draggable
}
@@ -46,7 +46,7 @@ final class MyTossable: Interaction {
//: Using our new interaction is a matter of instantiating it and associating it with a view:
-let tossable = MyTossable(spring: .init(threshold: 1, system: coreAnimation))
+let tossable = MyTossable()
runtime.add(tossable, to: view)
runtime.add(SetPositionOnTap(), to: tossable.spring.destination)
diff --git a/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to use springs.xcplaygroundpage/Contents.swift b/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to use springs.xcplaygroundpage/Contents.swift
index ef218ae..c6306cc 100644
--- a/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to use springs.xcplaygroundpage/Contents.swift
+++ b/examples/apps/Catalog/ReactivePlayground.playground/Pages/How to use springs.xcplaygroundpage/Contents.swift
@@ -21,7 +21,7 @@ let runtime = MotionRuntime(containerView: canvas)
let position = runtime.get(view.layer).position
//: Next we'll create our Spring interaction. We must specify the type of Spring we'd like to use because Spring is a [generic type](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Generics.html). In this case we want to animate a CGPoint, so we'll define that here:
-let spring = Spring(threshold: 1, system: coreAnimation)
+let spring = Spring()
//: Starting the spring animation is now a simple matter of adding it to the property:
runtime.add(spring, to: position)
diff --git a/src/MotionRuntime.swift b/src/MotionRuntime.swift
index ea37d77..782abc5 100644
--- a/src/MotionRuntime.swift
+++ b/src/MotionRuntime.swift
@@ -61,8 +61,8 @@ public final class MotionRuntime {
runtime.
*/
public func add(_ interaction: I, to target: I.Target, constraints: I.Constraints? = nil) {
- interaction.add(to: target, withRuntime: self, constraints: constraints)
interactions.append(interaction)
+ interaction.add(to: target, withRuntime: self, constraints: constraints)
}
/**
diff --git a/src/ReactiveProperty.swift b/src/ReactiveProperty.swift
index 95163b8..214c0c6 100644
--- a/src/ReactiveProperty.swift
+++ b/src/ReactiveProperty.swift
@@ -113,8 +113,19 @@ public final class ReactiveProperty {
func coreAnimation(_ event: CoreAnimationChannelEvent) {
_coreAnimation?(event)
+ let transformedEvent: CoreAnimationChannelEvent
+ switch event {
+ case .add(var info):
+ // This is a hack-fix to ensure that animations don't over-complete they're connected to other
+ // properties.
+ // Related to https://github.com/material-motion/material-motion-swift/issues/65
+ info.onCompletion = nil
+ transformedEvent = .add(info)
+ default:
+ transformedEvent = event
+ }
for observer in observers {
- observer.coreAnimation?(event)
+ observer.coreAnimation?(transformedEvent)
}
}
diff --git a/src/interactions/ChangeDirection.swift b/src/interactions/ChangeDirection.swift
new file mode 100644
index 0000000..0e1dfc9
--- /dev/null
+++ b/src/interactions/ChangeDirection.swift
@@ -0,0 +1,97 @@
+/*
+ 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
+
+/**
+ Changes the direction of a transition when the provided pan gesture recognizer completes.
+
+ Without any configuration, will set the direction to backward if the absolute velocity exceeds
+ minimumThreshold on the y axis.
+
+ **Common configurations**
+
+ *Modal dialogs and sheets*: cancel a dismissal when tossing up using `whenNegative: .forward`.
+
+ **Constraints**
+
+ Either the x or y axis can be selected. The default axis is y.
+ */
+public final class ChangeDirection: Interaction {
+ /**
+ The gesture recognizer that will be observed by this interaction.
+ */
+ public let gesture: UIPanGestureRecognizer
+
+ /**
+ The minimum absolute velocity required change the transition's direction.
+
+ If this velocity is not met, the direction will not be changed.
+ */
+ public let minimumVelocity: CGFloat
+
+ /**
+ The transition direction to emit when the velocity is below -minimumVelocity.
+ */
+ public let whenNegative: TransitionDirection
+
+ /**
+ The transition direction to emit when the velocity is above minimumVelocity.
+ */
+ public let whenPositive: TransitionDirection
+
+ /**
+ - 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) {
+ self.gesture = gesture
+ self.minimumVelocity = minimumVelocity
+ self.whenNegative = whenNegative
+ self.whenPositive = whenPositive
+ }
+
+ /**
+ The velocity axis to observe.
+ */
+ public enum Axis {
+ /**
+ Observes the velocity's x axis.
+ */
+ case x
+
+ /**
+ Observes the velocity's y axis.
+ */
+ case y
+ }
+
+ public func add(to direction: ReactiveProperty, withRuntime runtime: MotionRuntime, constraints axis: Axis?) {
+ let axis = axis ?? .y
+ let chooseAxis: (MotionObservable) -> MotionObservable
+ switch axis {
+ case .x:
+ chooseAxis = { $0.x() }
+ case .y:
+ chooseAxis = { $0.y() }
+ }
+ runtime.connect(chooseAxis(runtime.get(gesture).velocityOnReleaseStream())
+ .thresholdRange(min: -minimumVelocity, max: minimumVelocity)
+ .rewrite([.below: whenNegative, .above: whenPositive]),
+ to: direction)
+
+ }
+}
diff --git a/src/interactions/SlopRegion.swift b/src/interactions/SlopRegion.swift
new file mode 100644
index 0000000..01da615
--- /dev/null
+++ b/src/interactions/SlopRegion.swift
@@ -0,0 +1,73 @@
+/*
+ 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
+
+/**
+ Changes the direction of a transition when the provided pan gesture recognizer moves out of or back
+ into a slop region.
+
+ **Constraints**
+
+ Either the x or y axis can be selected. The default axis is y.
+ */
+public final class SlopRegion: Interaction {
+ /**
+ The gesture recognizer that will be observed by this interaction.
+ */
+ public let gesture: UIPanGestureRecognizer
+
+ /**
+ The size of the slop region.
+ */
+ public let size: CGFloat
+
+ public init(withTranslationOf gesture: UIPanGestureRecognizer, size: CGFloat) {
+ self.gesture = gesture
+ self.size = size
+ }
+
+ /**
+ The axis to observe.
+ */
+ public enum Axis {
+ /**
+ Observes the x axis.
+ */
+ case x
+
+ /**
+ Observes the y axis.
+ */
+ case y
+ }
+
+ public func add(to direction: ReactiveProperty, withRuntime runtime: MotionRuntime, constraints axis: Axis?) {
+ let axis = axis ?? .y
+ let chooseAxis: (MotionObservable) -> MotionObservable
+ switch axis {
+ case .x:
+ chooseAxis = { $0.x() }
+ case .y:
+ chooseAxis = { $0.y() }
+ }
+
+ runtime.connect(chooseAxis(runtime.get(gesture).translation(in: runtime.containerView))
+ .slop(size: size).rewrite([.onExit: .backward, .onReturn: .forward]),
+ to: direction)
+ }
+}
diff --git a/src/interactions/Spring.swift b/src/interactions/Spring.swift
index 325f608..92726ad 100644
--- a/src/interactions/Spring.swift
+++ b/src/interactions/Spring.swift
@@ -45,14 +45,14 @@ public let defaultSpringMass: CGFloat = 1
T-value constraints may be applied to this interaction.
*/
-public class Spring: Interaction, Togglable, Stateful {
+public class Spring: Interaction, Togglable, Stateful where T: Zeroable, T: Subtractable {
/**
Creates a spring with a given threshold and system.
- - parameter threshold: The threshold of movement defining the completion of the spring simulation.
- - parameter system: Often coreAnimation. Can be another system if a system support library is available.
+ - parameter threshold: The threshold of movement defining the completion of the spring simulation. This parameter is not used by the Core Animation system and can be left as a default value.
+ - parameter system: The system that should be used to drive this spring.
*/
- public init(threshold: CGFloat, system: @escaping SpringToStream) {
+ public init(threshold: CGFloat = 1, system: @escaping SpringToStream = coreAnimation) {
self.threshold = createProperty("Spring.threshold", withInitialValue: threshold)
self.system = system
}
@@ -140,7 +140,7 @@ public class Spring: Interaction, Togglable, Stateful {
private var activeSprings = Set>()
}
-public struct SpringShadow: Hashable {
+public struct SpringShadow: Hashable where T: Zeroable, T: Subtractable {
public let enabled: ReactiveProperty
public let state = createProperty(withInitialValue: MotionState.atRest)
public let initialValue: ReactiveProperty
diff --git a/src/interactions/Tossable.swift b/src/interactions/Tossable.swift
index 494966f..aa0d6f9 100644
--- a/src/interactions/Tossable.swift
+++ b/src/interactions/Tossable.swift
@@ -48,12 +48,7 @@ public final class Tossable: Interaction, Stateful {
*/
public let spring: Spring
- public init(system: @escaping SpringToStream = coreAnimation, draggable: Draggable = Draggable()) {
- self.spring = Spring(threshold: 1, system: system)
- self.draggable = draggable
- }
-
- public init(spring: Spring, draggable: Draggable = Draggable()) {
+ public init(spring: Spring = Spring(), draggable: Draggable = Draggable()) {
self.spring = spring
self.draggable = draggable
}
@@ -91,4 +86,9 @@ 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/interactions/TransitionSpring.swift b/src/interactions/TransitionSpring.swift
index 515a9ff..c661042 100644
--- a/src/interactions/TransitionSpring.swift
+++ b/src/interactions/TransitionSpring.swift
@@ -17,6 +17,26 @@
import Foundation
import UIKit
+/**
+ The default transition spring tension configuration.
+ */
+public let defaultTransitionSpringTension: CGFloat = 500
+
+/**
+ The default transition spring friction configuration.
+ */
+public let defaultTransitionSpringFriction: CGFloat = 1000
+
+/**
+ The default transition spring mass configuration.
+ */
+public let defaultTransitionSpringMass: CGFloat = 3
+
+/**
+ The default transition spring suggested duration.
+ */
+public let defaultTransitionSpringSuggestedDuration: CGFloat = 0.5
+
/**
A transition spring pulls a value from one side of a transition to another.
@@ -41,7 +61,7 @@ import UIKit
T-value constraints may be applied to this interaction.
*/
-public final class TransitionSpring: Spring {
+public final class TransitionSpring: Spring where T: Zeroable, T: Subtractable {
/**
The destination when the transition is moving backward.
@@ -59,20 +79,26 @@ public final class TransitionSpring: Spring {
- parameter back: The destination to which the spring will pull the view when transitioning backward.
- parameter fore: The destination to which the spring will pull the view when transitioning forward.
- parameter direction: The spring will change its destination in reaction to this property's changes.
- - parameter threshold: The threshold of movement defining the completion of the spring simulation.
- - parameter system: Often coreAnimation. Can be another system if a system support library is available.
+ - parameter threshold: The threshold of movement defining the completion of the spring simulation. This parameter is not used by the Core Animation system and can be left as a default value.
+ - parameter system: The system that should be used to drive this spring.
*/
public init(back backwardDestination: T,
fore forwardDestination: T,
direction: ReactiveProperty,
- threshold: CGFloat,
- system: @escaping SpringToStream) {
+ threshold: CGFloat = 1,
+ system: @escaping SpringToStream = coreAnimation) {
self.backwardDestination = backwardDestination
self.forwardDestination = forwardDestination
self.initialValue = direction == .forward ? backwardDestination : forwardDestination
self.toggledDestination = direction.rewrite([.backward: backwardDestination, .forward: forwardDestination])
super.init(threshold: threshold, system: system)
+
+ // Apply Core Animation transition spring defaults.
+ friction.value = defaultTransitionSpringTension
+ tension.value = defaultTransitionSpringFriction
+ mass.value = defaultTransitionSpringMass
+ suggestedDuration.value = defaultTransitionSpringSuggestedDuration
}
public override func add(to property: ReactiveProperty,
diff --git a/src/protocols/Gesturable.swift b/src/protocols/Gesturable.swift
index 43038d1..94dc5b5 100644
--- a/src/protocols/Gesturable.swift
+++ b/src/protocols/Gesturable.swift
@@ -56,6 +56,25 @@ public class Gesturable {
self.init(.registerNewRecognizerToTargetView)
}
+ /**
+ Initializes the interaction with the first gesture recognizer that matches the interaction's T
+ type.
+ */
+ public convenience init(withFirstGestureIn gestures: O) where O: Sequence, O.Iterator.Element == UIGestureRecognizer {
+ var first: T? = nil
+ for gesture in gestures {
+ if let gesture = gesture as? T {
+ first = gesture
+ break
+ }
+ }
+ if let first = first {
+ self.init(.withExistingRecognizer(first))
+ } else {
+ self.init()
+ }
+ }
+
public init(_ config: GesturableConfiguration) {
self.config = config
diff --git a/src/protocols/Systems.swift b/src/protocols/Systems.swift
index 58ebe8f..5e0de15 100644
--- a/src/protocols/Systems.swift
+++ b/src/protocols/Systems.swift
@@ -36,7 +36,13 @@ public typealias ScrollViewToStream = (UIScrollView) -> MotionObservable = (TweenShadow) -> MotionObservable
+/**
+ Swift 3.1 does not allow generic typealiases to use protocol lists, so we define this composite
+ type instead.
+ */
+public protocol ZeroableAndSubtractable: Zeroable, Subtractable {}
+
/**
A spring-to-stream function creates a MotionObservable from a Spring and initial value stream.
*/
-public typealias SpringToStream = (SpringShadow) -> MotionObservable
+public typealias SpringToStream = (SpringShadow) -> MotionObservable
diff --git a/src/reactivetypes/MotionObservable.swift b/src/reactivetypes/MotionObservable.swift
index a91ec74..ee9a243 100644
--- a/src/reactivetypes/MotionObservable.swift
+++ b/src/reactivetypes/MotionObservable.swift
@@ -74,7 +74,7 @@ public struct CoreAnimationChannelAdd {
/**
A completion handler, fired once Core Animation reports that the animation has completed.
*/
- public let onCompletion: () -> Void
+ public var onCompletion: (() -> Void)?
/**
The initial velocity of the animation, if relevant.
diff --git a/src/reactivetypes/ReactiveCALayer.swift b/src/reactivetypes/ReactiveCALayer.swift
index fe91ef9..1b83d8b 100644
--- a/src/reactivetypes/ReactiveCALayer.swift
+++ b/src/reactivetypes/ReactiveCALayer.swift
@@ -207,14 +207,27 @@ public func createCoreAnimationProperty(_ name: String, initialValue: T, exte
animation.keyPath = keyPath
- if let makeAdditive = info.makeAdditive, 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 makeAdditive = info.makeAdditive, let keyframeAnimation = animation as? CAKeyframeAnimation {
- let lastValue = keyframeAnimation.values!.last!
- keyframeAnimation.values = keyframeAnimation.values!.map { makeAdditive($0, lastValue) }
- keyframeAnimation.isAdditive = true
+ 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
@@ -243,7 +256,7 @@ public func createCoreAnimationProperty(_ name: String, initialValue: T, exte
CATransaction.begin()
CATransaction.setCompletionBlock(info.onCompletion)
- layer.add(animation, forKey: info.key)
+ layer.add(animation, forKey: info.key + "." + keyPath)
CATransaction.commit()
case .remove(let key):
@@ -256,7 +269,7 @@ public func createCoreAnimationProperty(_ name: String, initialValue: T, exte
strongReactiveLayer.decomposedKeys.remove(key)
} else {
- layer.removeAnimation(forKey: key)
+ layer.removeAnimation(forKey: key + "." + keyPath)
}
}
})
diff --git a/src/transitions/ViewControllerDismisser.swift b/src/transitions/ViewControllerDismisser.swift
index 883fd4e..009782c 100644
--- a/src/transitions/ViewControllerDismisser.swift
+++ b/src/transitions/ViewControllerDismisser.swift
@@ -33,6 +33,24 @@ public final class ViewControllerDismisser: NSObject {
soloGestureRecognizers.insert(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 {
+ for delegate in scrollViewTopEdgeDismisserDelegates {
+ if delegate.scrollView == scrollView {
+ return delegate
+ }
+ }
+ let delegate = ScrollViewTopEdgeDismisserDelegate()
+ delegate.scrollView = scrollView
+ scrollViewTopEdgeDismisserDelegates.append(delegate)
+ return delegate
+ }
+
@objc func gestureRecognizerDidChange(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .recognized {
delegate?.dismiss()
@@ -42,6 +60,20 @@ public final class ViewControllerDismisser: NSObject {
weak var delegate: ViewControllerDismisserDelegate?
private(set) var gestureRecognizers = Set()
fileprivate var soloGestureRecognizers = Set()
+
+ private var scrollViewTopEdgeDismisserDelegates: [ScrollViewTopEdgeDismisserDelegate] = []
+}
+
+private final class ScrollViewTopEdgeDismisserDelegate: NSObject, UIGestureRecognizerDelegate {
+ func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+ if let pan = gestureRecognizer as? UIPanGestureRecognizer, let scrollView = scrollView {
+ return pan.translation(in: pan.view).y > 0
+ && scrollView.contentOffset.y <= -scrollView.contentInset.top
+ }
+ return false
+ }
+
+ weak var scrollView: UIScrollView?
}
extension ViewControllerDismisser: UIGestureRecognizerDelegate {