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)