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

Commit

Permalink
Add a Manipulation type and implement UIKit view controller transitio…
Browse files Browse the repository at this point in the history
…ning 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
  • Loading branch information
Jeff Verkoeyen committed Apr 24, 2017
1 parent a921f72 commit 9fa2b22
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 9 deletions.
4 changes: 4 additions & 0 deletions examples/ContextualTransitionExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions src/Interaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
13 changes: 13 additions & 0 deletions src/MotionRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<Bool> {
return aggregateManipulationState.asStream().rewrite([.active: true, .atRest: false])
}
private let aggregateManipulationState = AggregateMotionState()

private func write<O: MotionObservableConvertible, T>(_ stream: O, to property: ReactiveProperty<T>) where O.T == T {
metadata.append(stream.metadata.createChild(property.metadata))
subscriptions.append(stream.subscribe(next: { property.value = $0 },
Expand Down
2 changes: 1 addition & 1 deletion src/interactions/Draggable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import UIKit
- `{ $0.xLocked(to: somePosition) }`
- `{ $0.yLocked(to: somePosition) }`
*/
public final class Draggable: Gesturable<UIPanGestureRecognizer>, Interaction, Togglable, Stateful {
public final class Draggable: Gesturable<UIPanGestureRecognizer>, Interaction, Togglable, Manipulation {
/**
A sub-interaction for writing the next gesture recognizer's final velocity to a property.

Expand Down
2 changes: 1 addition & 1 deletion src/interactions/Rotatable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import UIKit

CGFloat constraints may be applied to this interaction.
*/
public final class Rotatable: Gesturable<UIRotationGestureRecognizer>, Interaction, Togglable, Stateful {
public final class Rotatable: Gesturable<UIRotationGestureRecognizer>, Interaction, Togglable, Manipulation {
public func add(to view: UIView,
withRuntime runtime: MotionRuntime,
constraints applyConstraints: ConstraintApplicator<CGFloat>? = nil) {
Expand Down
2 changes: 1 addition & 1 deletion src/interactions/Scalable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import UIKit

CGFloat constraints may be applied to this interaction.
*/
public final class Scalable: Gesturable<UIPinchGestureRecognizer>, Interaction, Togglable, Stateful {
public final class Scalable: Gesturable<UIPinchGestureRecognizer>, Interaction, Togglable, Manipulation {
public func add(to view: UIView,
withRuntime runtime: MotionRuntime,
constraints applyConstraints: ConstraintApplicator<CGFloat>? = nil) {
Expand Down
47 changes: 41 additions & 6 deletions src/transitions/TransitionContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -228,19 +232,50 @@ 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

// 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)

Expand Down

0 comments on commit 9fa2b22

Please sign in to comment.