Commit 732bcdb5 by Daniel Dahan

progress on new Motion structure

parent 3d9641b4
......@@ -7,6 +7,9 @@
objects = {
/* Begin PBXBuildFile section */
965FE9611FDCC3AF0098BDD0 /* MotionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965FE9601FDCC3AF0098BDD0 /* MotionState.swift */; };
965FE9631FDCCE030098BDD0 /* Motion+Complete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965FE9621FDCCE030098BDD0 /* Motion+Complete.swift */; };
965FE9651FDCCE910098BDD0 /* MotionProgressRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965FE9641FDCCE910098BDD0 /* MotionProgressRunner.swift */; };
96E409651F24F7370015A2B5 /* MotionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E4093D1F24F7370015A2B5 /* MotionAnimator.swift */; };
96E409661F24F7370015A2B5 /* MotionAnimatorViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E4093E1F24F7370015A2B5 /* MotionAnimatorViewContext.swift */; };
96E409671F24F7370015A2B5 /* MotionCoreAnimationViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E4093F1F24F7370015A2B5 /* MotionCoreAnimationViewContext.swift */; };
......@@ -28,9 +31,7 @@
96E409771F24F7370015A2B5 /* MotionAnimationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E409521F24F7370015A2B5 /* MotionAnimationState.swift */; };
96E409781F24F7370015A2B5 /* MotionCAAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E409531F24F7370015A2B5 /* MotionCAAnimation.swift */; };
96E409791F24F7370015A2B5 /* MotionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E409541F24F7370015A2B5 /* MotionContext.swift */; };
96E4097A1F24F7370015A2B5 /* MotionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E409551F24F7370015A2B5 /* MotionController.swift */; };
96E4097B1F24F7370015A2B5 /* MotionCoordinateSpace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E409561F24F7370015A2B5 /* MotionCoordinateSpace.swift */; };
96E4097C1F24F7370015A2B5 /* MotionIndependentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E409571F24F7370015A2B5 /* MotionIndependentController.swift */; };
96E4097D1F24F7370015A2B5 /* MotionPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E409581F24F7370015A2B5 /* MotionPlugin.swift */; };
96E4097E1F24F7370015A2B5 /* MotionSnapshotType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E409591F24F7370015A2B5 /* MotionSnapshotType.swift */; };
96E4097F1F24F7370015A2B5 /* MotionTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E4095A1F24F7370015A2B5 /* MotionTransition.swift */; };
......@@ -63,9 +64,7 @@
96E4099C1F24F7570015A2B5 /* MotionAnimationState.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409521F24F7370015A2B5 /* MotionAnimationState.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E4099D1F24F7570015A2B5 /* MotionCAAnimation.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409531F24F7370015A2B5 /* MotionCAAnimation.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E4099E1F24F7570015A2B5 /* MotionContext.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409541F24F7370015A2B5 /* MotionContext.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E4099F1F24F7570015A2B5 /* MotionController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409551F24F7370015A2B5 /* MotionController.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E409A01F24F7570015A2B5 /* MotionCoordinateSpace.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409561F24F7370015A2B5 /* MotionCoordinateSpace.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E409A11F24F7570015A2B5 /* MotionIndependentController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409571F24F7370015A2B5 /* MotionIndependentController.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E409A21F24F7570015A2B5 /* MotionPlugin.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409581F24F7370015A2B5 /* MotionPlugin.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E409A31F24F7570015A2B5 /* MotionSnapshotType.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409591F24F7370015A2B5 /* MotionSnapshotType.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E409A41F24F7570015A2B5 /* MotionTransition.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E4095A1F24F7370015A2B5 /* MotionTransition.swift */; settings = {ATTRIBUTES = (Public, ); }; };
......@@ -81,6 +80,9 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
965FE9601FDCC3AF0098BDD0 /* MotionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionState.swift; sourceTree = "<group>"; };
965FE9621FDCCE030098BDD0 /* Motion+Complete.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Motion+Complete.swift"; sourceTree = "<group>"; };
965FE9641FDCCE910098BDD0 /* MotionProgressRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionProgressRunner.swift; sourceTree = "<group>"; };
96C98DD11E424AB000B22906 /* Motion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Motion.framework; sourceTree = BUILT_PRODUCTS_DIR; };
96E4093D1F24F7370015A2B5 /* MotionAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimator.swift; sourceTree = "<group>"; };
96E4093E1F24F7370015A2B5 /* MotionAnimatorViewContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimatorViewContext.swift; sourceTree = "<group>"; };
......@@ -103,9 +105,7 @@
96E409521F24F7370015A2B5 /* MotionAnimationState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimationState.swift; sourceTree = "<group>"; };
96E409531F24F7370015A2B5 /* MotionCAAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionCAAnimation.swift; sourceTree = "<group>"; };
96E409541F24F7370015A2B5 /* MotionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionContext.swift; sourceTree = "<group>"; };
96E409551F24F7370015A2B5 /* MotionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionController.swift; sourceTree = "<group>"; };
96E409561F24F7370015A2B5 /* MotionCoordinateSpace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionCoordinateSpace.swift; sourceTree = "<group>"; };
96E409571F24F7370015A2B5 /* MotionIndependentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionIndependentController.swift; sourceTree = "<group>"; };
96E409581F24F7370015A2B5 /* MotionPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionPlugin.swift; sourceTree = "<group>"; };
96E409591F24F7370015A2B5 /* MotionSnapshotType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionSnapshotType.swift; sourceTree = "<group>"; };
96E4095A1F24F7370015A2B5 /* MotionTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionTransition.swift; sourceTree = "<group>"; };
......@@ -146,13 +146,14 @@
96E409BC1F24FC300015A2B5 /* LICENSE */,
96E4094F1F24F7370015A2B5 /* Motion.h */,
96E409501F24F7370015A2B5 /* Motion.swift */,
965FE9601FDCC3AF0098BDD0 /* MotionState.swift */,
965FE9621FDCCE030098BDD0 /* Motion+Complete.swift */,
965FE9641FDCCE910098BDD0 /* MotionProgressRunner.swift */,
96E409511F24F7370015A2B5 /* MotionAnimation.swift */,
96E409521F24F7370015A2B5 /* MotionAnimationState.swift */,
96E409531F24F7370015A2B5 /* MotionCAAnimation.swift */,
96E409541F24F7370015A2B5 /* MotionContext.swift */,
96E409551F24F7370015A2B5 /* MotionController.swift */,
96E409561F24F7370015A2B5 /* MotionCoordinateSpace.swift */,
96E409571F24F7370015A2B5 /* MotionIndependentController.swift */,
96E409581F24F7370015A2B5 /* MotionPlugin.swift */,
96E409591F24F7370015A2B5 /* MotionSnapshotType.swift */,
96E4095A1F24F7370015A2B5 /* MotionTransition.swift */,
......@@ -235,9 +236,7 @@
96E4099C1F24F7570015A2B5 /* MotionAnimationState.swift in Headers */,
96E4099D1F24F7570015A2B5 /* MotionCAAnimation.swift in Headers */,
96E4099E1F24F7570015A2B5 /* MotionContext.swift in Headers */,
96E4099F1F24F7570015A2B5 /* MotionController.swift in Headers */,
96E409A01F24F7570015A2B5 /* MotionCoordinateSpace.swift in Headers */,
96E409A11F24F7570015A2B5 /* MotionIndependentController.swift in Headers */,
96E409A21F24F7570015A2B5 /* MotionPlugin.swift in Headers */,
96E409A31F24F7570015A2B5 /* MotionSnapshotType.swift in Headers */,
96E409A41F24F7570015A2B5 /* MotionTransition.swift in Headers */,
......@@ -314,11 +313,11 @@
96E409731F24F7370015A2B5 /* MotionAnimationFillMode.swift in Sources */,
96E409791F24F7370015A2B5 /* MotionContext.swift in Sources */,
96E409831F24F7370015A2B5 /* DurationPreprocessor.swift in Sources */,
965FE9631FDCCE030098BDD0 /* Motion+Complete.swift in Sources */,
96E4097D1F24F7370015A2B5 /* MotionPlugin.swift in Sources */,
96E409681F24F7370015A2B5 /* MotionHasInsertOrder.swift in Sources */,
96E4096E1F24F7370015A2B5 /* Motion+CG.swift in Sources */,
96E409851F24F7370015A2B5 /* MatchPreprocessor.swift in Sources */,
96E4097C1F24F7370015A2B5 /* MotionIndependentController.swift in Sources */,
96E409861F24F7370015A2B5 /* MotionPreprocessor.swift in Sources */,
96E409821F24F7370015A2B5 /* CascadePreprocessor.swift in Sources */,
96E4096C1F24F7370015A2B5 /* Motion+CALayer.swift in Sources */,
......@@ -329,7 +328,6 @@
96E409651F24F7370015A2B5 /* MotionAnimator.swift in Sources */,
96E409671F24F7370015A2B5 /* MotionCoreAnimationViewContext.swift in Sources */,
96E4096D1F24F7370015A2B5 /* Motion+CAMediaTimingFunction.swift in Sources */,
96E4097A1F24F7370015A2B5 /* MotionController.swift in Sources */,
96E409841F24F7370015A2B5 /* IgnoreSubviewModifiersPreprocessor.swift in Sources */,
96E409771F24F7370015A2B5 /* MotionAnimationState.swift in Sources */,
96E409761F24F7370015A2B5 /* MotionAnimation.swift in Sources */,
......@@ -342,6 +340,8 @@
96E409881F24F7370015A2B5 /* TransitionPreprocessor.swift in Sources */,
96E4096A1F24F7370015A2B5 /* MotionViewPropertyViewContext.swift in Sources */,
96E409661F24F7370015A2B5 /* MotionAnimatorViewContext.swift in Sources */,
965FE9651FDCCE910098BDD0 /* MotionProgressRunner.swift in Sources */,
965FE9611FDCC3AF0098BDD0 /* MotionState.swift in Sources */,
96E4097F1F24F7370015A2B5 /* MotionTransition.swift in Sources */,
96E409751F24F7370015A2B5 /* Motion.swift in Sources */,
96E4096F1F24F7370015A2B5 /* Motion+Obj-C.swift in Sources */,
......
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
LastUpgradeVersion = "0920"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
......
......@@ -220,6 +220,7 @@ internal class SnapshotWrapperView: UIView {
init(contentView: UIView) {
self.contentView = contentView
super.init(frame: contentView.frame)
contentView.frame = bounds
addSubview(contentView)
}
......@@ -229,8 +230,7 @@ internal class SnapshotWrapperView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
contentView.bounds.size = bounds.size
contentView.center = bounds.center
contentView.frame = bounds
}
}
......@@ -243,13 +243,17 @@ internal extension UIView {
if #available(iOS 9.0, *), isHidden && (superview is UICollectionView || superview is UIStackView || self is UITableViewCell) {
return []
} else if isHidden && (superview is UICollectionView || self is UITableViewCell) {
return []
} else if isMotionEnabledForSubviews {
return [self] + subviews.flatMap { $0.flattenedViewHierarchy }
} else {
return [self]
return [self] + subviews.flatMap {
$0.flattenedViewHierarchy
}
}
return [self]
}
/**
......@@ -285,7 +289,13 @@ internal extension UIView {
*/
func slowSnapshotView() -> UIView {
UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
layer.render(in: UIGraphicsGetCurrentContext()!)
guard let currentContext = UIGraphicsGetCurrentContext() else {
UIGraphicsEndImageContext()
return UIView()
}
layer.render(in: currentContext)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
......
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
extension Motion {
/**
Complete the transition.
- Parameter after: A TimeInterval.
- Parameter isFinished: A Boolean indicating if the transition
has completed.
*/
func complete(after: TimeInterval, isFinished: Bool) {
guard isTransitioning else {
return
}
if after <= 0.001 {
complete(isFinished: isFinished)
return
}
let v = (isFinished ? elapsedTime : 1 - elapsedTime) * totalDuration
self.isFinished = isFinished
currentAnimationDuration = after + v
beginTime = CACurrentMediaTime() - v
}
/**
Complete the transition.
- Parameter isFinished: A Boolean indicating if the transition
has completed.
*/
@objc
func complete(isFinished: Bool) {
if state == .notified {
forceFinishing = isFinished
}
guard .animating == state || .starting == state else {
return
}
defer {
transitionContext = nil
fromViewController = nil
toViewController = nil
isNavigationController = false
isTabBarController = false
forceNonInteractive = false
transitionPairs.removeAll()
transitionObservers = nil
transitionContainer = nil
completionCallback = nil
forceFinishing = nil
container = nil
startingProgress = nil
preprocessors.removeAll()
animators.removeAll()
plugins.removeAll()
context = nil
elapsedTime = 0
totalDuration = 0
state = .possible
}
state = .completing
progressRunner.stop()
context.clean()
if let tv = toView, let fv = fromView {
if isFinished && isPresenting && toOverFullScreen {
// finished presenting a overFullScreen view controller.
context.unhide(rootView: tv)
context.removeSnapshots(rootView: tv)
context.storeViewAlpha(rootView: fv)
fromViewController!.motionStoredSnapshot = container
fv.removeFromSuperview()
fv.addSubview(container)
} else if !isFinished && !isPresenting && fromOverFullScreen {
// Cancelled dismissing a overFullScreen view controller.
context.unhide(rootView: fv)
context.removeSnapshots(rootView: fv)
context.storeViewAlpha(rootView: tv)
toViewController!.motionStoredSnapshot = container
container.superview?.addSubview(tv)
tv.addSubview(container)
} else {
context.unhideAll()
context.removeAllSnapshots()
}
// Move fromView & toView back from our container back to the one supplied by UIKit.
if (toOverFullScreen && isFinished) || (fromOverFullScreen && !isFinished) {
transitionContainer?.addSubview(isFinished ? fv : tv)
}
transitionContainer?.addSubview(isFinished ? tv : fv)
if isPresenting != isFinished, !isContainerController {
// Only happens when present a .overFullScreen view controller.
// bug: http://openradar.appspot.com/radar?id=5320103646199808
UIApplication.shared.keyWindow?.addSubview(isPresenting ? fv : tv)
}
}
if container.superview == transitionContainer {
container.removeFromSuperview()
}
for a in animators {
a.clean()
}
transitionContainer?.isUserInteractionEnabled = true
completionCallback?(isFinished)
let tContext = transitionContext
let fvc = fromViewController
let tvc = toViewController
if isFinished {
processEndTransitionDelegation(transitionContext: tContext, fromViewController: fvc, toViewController: tvc)
} else {
processCancelTransitionDelegation(transitionContext: tContext, fromViewController: fvc, toViewController: tvc)
tContext?.cancelInteractiveTransition()
}
tContext?.completeTransition(isFinished)
}
}
......@@ -125,54 +125,176 @@ public protocol MotionViewControllerDelegate {
func apply(transitions: [MotionTransition], to view: UIView)
```
*/
public class Motion: MotionController {
import UIKit
public class Motion: NSObject, MotionProgressRunnerDelegate {
/// Shared singleton object for controlling the transition
public static let shared = Motion()
/// Plugins that are enabled during the transition.
internal static var enabledPlugins = [MotionPlugin.Type]()
/// A reference to a fullscreen snapshot.
internal var fullScreenSnapshot: UIView!
/// A reference to the MotionContext.
public internal(set) var context: MotionContext!
/// A boolean indicating whether the transition interactive or not.
public var isInteractive: Bool {
return nil == displayLink
}
/// Source view controller.
public internal(set) var fromViewController: UIViewController?
/// Destination view controller.
public internal(set) var toViewController: UIViewController?
/// Whether or not we are presenting the destination view controller.
public internal(set) var isPresenting = true
/// A reference to the fromView, fromViewController.view.
internal var fromView: UIView? {
return fromViewController?.view
}
/// A reference to the toView, toViewController.view.
internal var toView: UIView? {
return toViewController?.view
}
/// The color of the transitioning container.
internal var containerBackgroundColor: UIColor?
/**
A UIViewControllerContextTransitioning object provided by UIKit, which
might be nil when isTransitioning. This happens when calling motionReplaceViewController
*/
internal weak var transitionContext: UIViewControllerContextTransitioning?
/// Progress of the current transition, 0 if a transition is not happening.
public override var elapsedTime: TimeInterval {
/// Progress of the current transition. 0 if no transition is happening.
public internal(set) var elapsedTime: TimeInterval = 0 {
didSet {
guard isTransitioning else {
guard .animating == state else {
return
}
updateTransitionObservers()
if isInteractive {
updateAnimators()
} else {
updatePlugins()
}
transitionContext?.updateInteractiveTransition(CGFloat(elapsedTime))
}
}
/// Indicates whether the transition is animating or not.
public var isAnimating = false
/// State of the transition.
public internal(set) var state = MotionState.possible {
didSet {
guard .notified != state else {
return
}
guard .starting != state else {
return
}
beginCallback?(.animating == state)
beginCallback = nil
}
}
/// A boolean indicating whether a transition is active.
public var isTransitioning: Bool {
return nil != transitionContainer
}
/**
A UIViewControllerContextTransitioning object provided by UIKit, which
might be nil when isTransitioning. This happens when calling motionReplaceViewController
A view container used to hold all the animating views during a
transition.
*/
internal weak var transitionContext: UIViewControllerContextTransitioning?
public internal(set) var container: UIView!
/// A reference to a fullscreen snapshot.
internal var fullScreenSnapshot: UIView!
/// UIKit's supplied transition container.
internal var transitionContainer: UIView?
/// An optional begin callbcak.
internal var beginCallback: ((Bool) -> Void)?
/// An optional completion callback.
internal var completionCallback: ((Bool) -> Void)?
/// Binds the render cycle to the transition animation.
internal var displayLink: CADisplayLink?
/// An Array of observers that are updated during a transition.
internal var transitionObservers: [MotionTransitionObserver]?
/// Max duration used by MotionAnimators and MotionPlugins.
public internal(set) var totalDuration: TimeInterval = 0
/// The currently running animation duration.
internal var currentAnimationDuration: TimeInterval = 0
/// A reference to a MotionProgressRunner.
lazy var progressRunner: MotionProgressRunner = {
let runner = MotionProgressRunner()
runner.delegate = self
return runner
}()
/// The start time of the animation.
internal var beginTime: TimeInterval? {
didSet {
guard nil != beginTime else {
displayLink?.isPaused = true
displayLink?.remove(from: RunLoop.main, forMode: RunLoopMode(rawValue: RunLoopMode.commonModes.rawValue))
displayLink = nil
return
}
guard nil == displayLink else {
return
}
displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink?.add(to: RunLoop.main, forMode: RunLoopMode(rawValue: RunLoopMode.commonModes.rawValue))
}
}
/// A boolean indicating if the transition has finished.
internal var isFinished = true
/// An Array of MotionPreprocessors used during a transition.
internal lazy var preprocessors = [MotionPreprocessor]()
/// An Array of MotionAnimators used during a transition.
internal lazy var animators = [MotionAnimator]()
/// An Array of MotionPlugins used during a transition.
internal lazy var plugins = [MotionPlugin]()
/// The matching fromViews to toViews based on the motionIdentifier value.
internal lazy var transitionPairs = [(fromViews: [UIView], toViews: [UIView])]()
/// Whether or not we are presenting the destination view controller.
public var isPresenting = true
/// Indicates whether the transition is animating or not.
public var isAnimating = false
/// Default animation type.
internal var defaultAnimation = MotionTransitionType.auto
/// The color of the transitioning container.
internal var containerBackgroundColor: UIColor?
/**
By default, Motion will always appear to be interactive to UIKit. This forces it to appear non-interactive.
Used when doing a motionReplaceViewController within a UINavigationController, to fix a bug with
UINavigationController.setViewControllers not able to handle interactive transitions.
*/
internal var forceNonInteractive = false
internal var forceFinishing: Bool?
internal var startingProgress: CGFloat?
/// Inserts the toViews first.
internal var insertToViewFirst = false
......@@ -206,16 +328,6 @@ public class Motion: MotionController {
return !isContainerController && (.overFullScreen == v.modalPresentationStyle || .overCurrentContext == v.modalPresentationStyle)
}
/// A reference to the fromView, fromViewController.view.
internal var fromView: UIView? {
return fromViewController?.view
}
/// A reference to the toView, toViewController.view.
internal var toView: UIView? {
return toViewController?.view
}
/// An initializer.
internal override init() {
super.init()
......@@ -223,169 +335,490 @@ public class Motion: MotionController {
}
public extension Motion {
/// Turn off built-in animations for the next transition.
func disableDefaultAnimationForNextTransition() {
defaultAnimation = .none
}
/**
Set the default animation for the next transition. This may override the
root-view's motionTransitions during the transition.
- Parameter animation: A MotionTransitionType.
Receive callbacks on each animation frame.
Observers will be cleaned when a transition completes.
- Parameter observer: A MotionTransitionObserver.
*/
func setAnimationForNextTransition(_ animation: MotionTransitionType) {
defaultAnimation = animation
func addTransitionObserver(observer: MotionTransitionObserver) {
if nil == transitionObservers {
transitionObservers = []
}
transitionObservers?.append(observer)
}
}
private extension Motion {
/// Updates the transition observers.
func updateTransitionObservers() {
guard let observers = transitionObservers else {
return
}
for v in observers {
v.motion(transitionObserver: v, didUpdateWith: elapsedTime)
}
}
/**
Set the container background color for the next transition.
- Parameter _ color: An optional UIColor.
*/
func setContainerBackgroundColorForNextTransition(_ color: UIColor?) {
containerBackgroundColor = color
/// Updates the animators.
func updateAnimators() {
let t = elapsedTime * totalDuration
for a in animators {
a.seek(to: t)
}
}
/// Updates the plugins.
func updatePlugins() {
let t = elapsedTime * totalDuration
for p in plugins where p.requirePerFrameCallback {
p.seek(to: t)
}
}
}
public extension Motion {
private extension Motion {
/**
A helper transition function.
- Parameter from: A UIViewController.
- Parameter to: A UIViewController.
- Parameter in view: A UIView.
- Parameter completion: An optional completion handler.
Handler for the DisplayLink updates.
- Parameter _ link: CADisplayLink.
*/
func transition(from: UIViewController, to: UIViewController, in view: UIView, completion: ((Bool) -> Void)? = nil) {
guard !isTransitioning else {
@objc
func handleDisplayLink(_ link: CADisplayLink) {
guard isTransitioning else {
return
}
isPresenting = true
transitionContainer = view
fromViewController = from
toViewController = to
completionCallback = completion
guard 0 < currentAnimationDuration else {
return
}
start()
guard let t = beginTime else {
return
}
let cTime = CACurrentMediaTime() - t
if cTime > currentAnimationDuration {
elapsedTime = isFinished ? 1 : 0
beginTime = nil
complete(isFinished: isFinished)
} else {
var eTime = cTime / totalDuration
if !isFinished {
eTime = 1 - eTime
}
elapsedTime = max(0, min(1, eTime))
}
}
}
fileprivate extension Motion {
/// Starts the transition animation.
func start() {
public extension Motion {
/**
Updates the elapsed time for the interactive transition.
- Parameter elapsedTime t: the current progress, must be between -1...1.
*/
public func update(elapsedTime: TimeInterval) {
guard isTransitioning else {
return
}
prepareViewControllers()
prepareSnapshotView()
prepareTransition()
prepareContext()
prepareToView()
prepareViewHierarchy()
processContext()
prepareTransitionPairs()
processForAnimation()
beginTime = nil
self.elapsedTime = max(-1, min(1, elapsedTime))
}
}
internal extension Motion {
override func animate() {
guard let tv = toView else {
/**
Finish the interactive transition.
Will stop the interactive transition and animate from the
current state to the **end** state
- Parameter isAnimated: A boolean indicating if the completion is animated.
*/
public func end(isAnimated: Bool = true) {
guard isTransitioning else {
return
}
context.unhide(view: tv)
guard isAnimated else {
complete(isFinished: true)
return
}
updateContainerBackgroundColor()
updateInsertOrder()
var t: TimeInterval = 0
super.animate()
for a in animators {
t = max(t, a.resume(at: elapsedTime * totalDuration, isReversed: false))
}
fullScreenSnapshot?.removeFromSuperview()
complete(after: t, isFinished: true)
}
override func complete(isFinished: Bool) {
/**
Cancel the interactive transition.
Will stop the interactive transition and animate from the
current state to the **begining** state
- Parameter isAnimated: A boolean indicating if the completion is animated.
*/
public func cancel(isAnimated: Bool = true) {
guard isTransitioning else {
return
}
guard let c = container else {
guard isAnimated else {
complete(isFinished: false)
return
}
guard let tc = transitionContainer else {
var d: TimeInterval = 0
for a in animators {
var t = elapsedTime
if t < 0 {
t = -t
}
d = max(d, a.resume(at: t * totalDuration, isReversed: true))
}
complete(after: d, isFinished: false)
}
/**
Override transition animations during an interactive animation.
For example:
Motion.shared.apply([.position(x:50, y:50)], to: view)
will set the view's position to 50, 50
- Parameter transitions: An Array of MotionTransitions.
- Parameter to view: A UIView.
*/
public func apply(transitions: [MotionTransition], to view: UIView) {
guard isTransitioning else {
return
}
guard let fv = fromView else {
let s = MotionTransitionState(transitions: transitions)
let v = context.transitionPairedView(for: view) ?? view
for a in animators {
a.apply(state: s, to: v)
}
}
}
internal extension Motion {
/**
Load plugins, processors, animators, container, & context
The transitionContainer must already be set.
Subclasses should call context.set(fromViews: toViews) after
inserting fromViews & toViews into the container
*/
@objc
func prepareTransition() {
guard isTransitioning else {
return
}
prepareTransitionContainer()
prepareContext()
preparePreprocessors()
prepareAnimators()
preparePlugins()
}
/// Prepares the transition fromView & toView pairs.
@objc
func prepareTransitionPairs() {
guard isTransitioning else {
return
}
for a in animators {
let fv = context.fromViews.filter { (view) -> Bool in
return a.canAnimate(view: view, isAppearing: false)
}
let tv = context.toViews.filter {
return a.canAnimate(view: $0, isAppearing: true)
}
transitionPairs.append((fv, tv))
}
guard let tv = toView else {
return
}
context.clean()
context.hide(view: tv)
}
}
internal extension Motion {
/// Executes the preprocessors' process function.
func processContext() {
guard isTransitioning else {
return
}
for x in preprocessors {
x.process(fromViews: context.fromViews, toViews: context.toViews)
}
}
/**
Animates the views. Subclasses should call `prepareTransition` &
`prepareTransitionPairs` before calling `animate`.
*/
@objc
func animate() {
guard .starting == state else {
return
}
state = .animating
if let tv = toView {
context.unhide(view: tv)
}
if isFinished && isPresenting && toOverFullScreen {
// finished presenting a overFullScreen view controller.
context.unhide(rootView: tv)
context.removeSnapshots(rootView: tv)
context.storeViewAlpha(rootView: fv)
for (fv, tv) in transitionPairs {
for view in fv {
context.hide(view: view)
}
fromViewController!.motionStoredSnapshot = container
fv.removeFromSuperview()
fv.addSubview(c)
} else if !isFinished && !isPresenting && fromOverFullScreen {
// Cancelled dismissing a overFullScreen view controller.
context.unhide(rootView: fv)
context.removeSnapshots(rootView: fv)
context.storeViewAlpha(rootView: tv)
for view in tv {
context.hide(view: view)
}
}
var t: TimeInterval = 0
var b = false
for (i, a) in animators.enumerated() {
let d = a.animate(fromViews: transitionPairs[i].0, toViews: transitionPairs[i].1)
toViewController!.motionStoredSnapshot = container
tv.removeFromSuperview()
tv.addSubview(c)
if .infinity == d {
b = true
} else {
t = max(t, d)
}
}
totalDuration = t
if b {
update(elapsedTime: 0)
} else {
context.unhideAll()
context.removeAllSnapshots()
c.removeFromSuperview()
complete(after: t, isFinished: true)
}
// Move fromView & toView back from our container back to the one supplied by UIKit.
if (toOverFullScreen && isFinished) || (fromOverFullScreen && !isFinished) {
tc.addSubview(isFinished ? fv : tv)
updateContainerBackgroundColor()
updateInsertOrder()
fullScreenSnapshot?.removeFromSuperview()
}
}
private extension Motion {
/// Prepares the transition container.
func prepareTransitionContainer() {
guard let v = transitionContainer else {
return
}
tc.addSubview(isFinished ? tv : fv)
v.isUserInteractionEnabled = false
// a view to hold all the animating views
container = UIView(frame: v.bounds)
v.addSubview(container!)
}
/// Prepares the preprocessors.
func preparePreprocessors() {
for x in [
IgnoreSubviewTransitionsPreprocessor(),
MatchPreprocessor(),
SourcePreprocessor(),
CascadePreprocessor(),
TransitionPreprocessor(motion: self),
DurationPreprocessor()] as [MotionPreprocessor] {
preprocessors.append(x)
}
if isPresenting != isFinished, !isContainerController {
// Only happens when present a .overFullScreen view controller.
// bug: http://openradar.appspot.com/radar?id=5320103646199808
UIApplication.shared.keyWindow!.addSubview(isPresenting ? fv : tv)
for x in preprocessors {
x.context = context
}
}
/// Prepares the animators.
func prepareAnimators() {
animators.append(MotionTransitionAnimator<MotionCoreAnimationViewContext>())
// use temp variables to remember these values
// because we have to reset everything before calling
// any delegate or completion block
let tContext = transitionContext
let fvc = fromViewController
let tvc = toViewController
if #available(iOS 10, tvOS 10, *) {
animators.append(MotionTransitionAnimator<MotionViewPropertyViewContext>())
}
resetTransition()
for v in animators {
v.context = context
}
}
/// Prepares the plugins.
func preparePlugins() {
for x in Motion.enabledPlugins.map({
return $0.init()
}) {
plugins.append(x)
}
super.complete(isFinished: isFinished)
for plugin in plugins {
preprocessors.append(plugin)
animators.append(plugin)
}
}
}
private extension Motion {
/// Updates the container background color.
func updateContainerBackgroundColor() {
if let v = containerBackgroundColor {
container?.backgroundColor = v
} else if !toOverFullScreen && !fromOverFullScreen {
container?.backgroundColor = toView?.backgroundColor
}
}
/// Updates the insertToViewFirst boolean for animators.
func updateInsertOrder() {
if fromOverFullScreen {
insertToViewFirst = true
}
if isFinished {
processEndTransitionDelegation(transitionContext: tContext, fromViewController: fvc, toViewController: tvc)
} else {
processCancelTransitionDelegation(transitionContext: tContext, fromViewController: fvc, toViewController: tvc)
for v in animators {
(v as? MotionHasInsertOrder)?.insertToViewFirst = insertToViewFirst
}
}
}
internal extension Motion {
/**
Checks if a given plugin is enabled.
- Parameter plugin: A MotionPlugin.Type.
- Returns: A boolean indicating if the plugin is enabled or not.
*/
static func isEnabled(plugin: MotionPlugin.Type) -> Bool {
return nil != enabledPlugins.index(where: { return $0 == plugin })
}
/**
Enables a given plugin.
- Parameter plugin: A MotionPlugin.Type.
*/
static func enable(plugin: MotionPlugin.Type) {
disable(plugin: plugin)
enabledPlugins.append(plugin)
}
/**
Disables a given plugin.
- Parameter plugin: A MotionPlugin.Type.
*/
static func disable(plugin: MotionPlugin.Type) {
guard let index = enabledPlugins.index(where: { return $0 == plugin }) else {
return
}
tContext?.completeTransition(isFinished)
enabledPlugins.remove(at: index)
}
}
fileprivate extension Motion {
internal extension Motion {
// should call this after `prepareTransitionPairs` & before `processContext`
func insert<T>(preprocessor: MotionPreprocessor, before: T.Type) {
let i = preprocessors.index { $0 is T } ?? preprocessors.count
preprocessor.context = context
preprocessors.insert(preprocessor, at: i)
}
}
public extension Motion {
/// Turn off built-in animations for the next transition.
func disableDefaultAnimationForNextTransition() {
defaultAnimation = .none
}
/**
Set the default animation for the next transition. This may override the
root-view's motionTransitions during the transition.
- Parameter animation: A MotionTransitionType.
*/
func setAnimationForNextTransition(_ animation: MotionTransitionType) {
defaultAnimation = animation
}
/**
Set the container background color for the next transition.
- Parameter _ color: An optional UIColor.
*/
func setContainerBackgroundColorForNextTransition(_ color: UIColor?) {
containerBackgroundColor = color
}
}
public extension Motion {
/**
A helper transition function.
- Parameter from: A UIViewController.
- Parameter to: A UIViewController.
- Parameter in view: A UIView.
- Parameter completion: An optional completion handler.
*/
func transition(from: UIViewController, to: UIViewController, in view: UIView, completion: ((Bool) -> Void)? = nil) {
guard !isTransitioning else {
return
}
isPresenting = true
transitionContainer = view
fromViewController = from
toViewController = to
completionCallback = completion
start()
}
}
private extension Motion {
/// Starts the transition animation.
func start() {
guard .notified == state else {
return
}
state = .starting
prepareViewControllers()
prepareSnapshotView()
prepareTransition()
prepareContext()
prepareToView()
prepareViewHierarchy()
processContext()
prepareTransitionPairs()
processForAnimation()
}
}
internal extension Motion {
/// Resets the transition values.
func resetTransition() {
transitionContext = nil
......@@ -400,7 +833,7 @@ fileprivate extension Motion {
}
}
fileprivate extension Motion {
internal extension Motion {
/// Prepares the from and to view controllers.
func prepareViewControllers() {
processStartTransitionDelegation(fromViewController: fromViewController, toViewController: toViewController)
......@@ -440,6 +873,8 @@ fileprivate extension Motion {
return
}
context = MotionContext(container: v)
context.loadViewAlpha(rootView: tv)
v.addSubview(tv)
......@@ -457,8 +892,12 @@ fileprivate extension Motion {
return
}
tv.frame = fv.frame
tv.updateConstraints()
if let toViewController = toViewController, let transitionContext = transitionContext {
tv.frame = transitionContext.finalFrame(for: toViewController)
} else {
tv.frame = fv.frame
}
tv.setNeedsLayout()
tv.layoutIfNeeded()
}
......@@ -478,27 +917,11 @@ fileprivate extension Motion {
}
internal extension Motion {
override func prepareTransition() {
super.prepareTransition()
insert(preprocessor: TransitionPreprocessor(motion: self), before: DurationPreprocessor.self)
}
override func prepareTransitionPairs() {
super.prepareTransitionPairs()
guard let tv = toView else {
return
}
context.hide(view: tv)
}
}
fileprivate extension Motion {
/// Processes the animations.
func processForAnimation() {
#if os(tvOS)
animate()
#else
if isNavigationController {
// When animating within navigationController, we have to dispatch later into the main queue.
......@@ -509,6 +932,7 @@ fileprivate extension Motion {
} else {
animate()
}
#endif
}
......@@ -627,28 +1051,6 @@ fileprivate extension Motion {
}
}
fileprivate extension Motion {
/// Updates the container background color.
func updateContainerBackgroundColor() {
if let v = containerBackgroundColor {
container?.backgroundColor = v
} else if !toOverFullScreen && !fromOverFullScreen {
container?.backgroundColor = toView?.backgroundColor
}
}
/// Updates the insertToViewFirst boolean for animators.
func updateInsertOrder() {
if fromOverFullScreen {
insertToViewFirst = true
}
for v in animators {
(v as? MotionHasInsertOrder)?.insertToViewFirst = insertToViewFirst
}
}
}
internal extension Motion {
/**
Helper for processing the MotionViewControllerDelegate.
......@@ -696,7 +1098,7 @@ extension Motion: UIViewControllerAnimatedTransitioning {
- Returns: A TimeInterval that is the total animation time including delays.
*/
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0 // Will be updated dynamically.
return 0 // Time will be updated dynamically.
}
public func animationEnded(_ transitionCompleted: Bool) {
......
......@@ -77,6 +77,7 @@ internal extension MotionContext {
func set(fromViews: [UIView], toViews: [UIView]) {
self.fromViews = fromViews
self.toViews = toViews
map(views: fromViews, identifierMap: &motionIdentifierToSourceView)
map(views: toViews, identifierMap: &motionIdentifierToDestinationView)
}
......@@ -114,8 +115,8 @@ public extension MotionContext {
get {
return viewToTargetState[view]
}
set {
viewToTargetState[view] = newValue
set(value) {
viewToTargetState[view] = value
}
}
}
......@@ -185,6 +186,7 @@ public extension MotionContext {
if let visualEffectView = containerView as? UIVisualEffectView {
containerView = visualEffectView.contentView
}
case .global:
break
}
......@@ -221,8 +223,8 @@ public extension MotionContext {
case .optimized:
#if os(tvOS)
snapshot = view.snapshotView(afterScreenUpdates: true)!
#else
#else
if #available(iOS 9.0, *), let stackView = view as? UIStackView {
snapshot = stackView.slowSnapshotView()
......@@ -246,7 +248,6 @@ public extension MotionContext {
// take a snapshot without the background
barView.layer.sublayers![0].opacity = 0
let realSnapshot = barView.snapshotView(afterScreenUpdates: true)!
barView.layer.sublayers![0].opacity = 1
......@@ -260,6 +261,7 @@ public extension MotionContext {
} else {
snapshot = view.snapshotView() ?? UIView()
}
#endif
}
......@@ -267,6 +269,7 @@ public extension MotionContext {
if let imageView = view as? UIImageView, imageView.adjustsImageWhenAncestorFocused {
snapshot.frame = imageView.focusedFrameGuide.layoutFrame
}
#endif
view.layer.cornerRadius = oldCornerRadius
......@@ -323,6 +326,7 @@ public extension MotionContext {
for sibling in nextSiblings {
insertGlobalViewTree(view: sibling)
}
} else {
containerView.addSubview(snapshot)
}
......@@ -363,7 +367,7 @@ internal extension MotionContext {
- Parameter view: A UIView.
*/
func hide(view: UIView) {
guard nil == viewToAlphas[view], .noSnapshot != self[view]?.snapshotType else {
guard nil == viewToAlphas[view] else {
return
}
......@@ -372,7 +376,7 @@ internal extension MotionContext {
viewToAlphas[view] = 1
} else {
viewToAlphas[view] = view.isOpaque ? .infinity : view.alpha
viewToAlphas[view] = view.alpha
view.alpha = 0
}
}
......@@ -389,10 +393,6 @@ internal extension MotionContext {
if view is UIVisualEffectView {
view.isHidden = false
} else if oldAlpha == .infinity {
view.alpha = 1
view.isOpaque = true
} else {
view.alpha = oldAlpha
}
......
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
public class MotionController: NSObject {
/// A reference to the MotionContext.
public internal(set) var context: MotionContext!
/// A boolean indicating whether the transition interactive or not.
public var isInteractive: Bool {
return nil == displayLink
}
/// Progress of the current transition. 0 if no transition is happening.
public internal(set) var elapsedTime: TimeInterval = 0 {
didSet {
guard isTransitioning else {
return
}
updateTransitionObservers()
guard isInteractive else {
updatePlugins()
return
}
updateAnimators()
}
}
/// A boolean indicating whether a transition is active.
public var isTransitioning: Bool {
return nil != transitionContainer
}
/**
A view container used to hold all the animating views during a
transition.
*/
public internal(set) var container: UIView?
/// UIKit's supplied transition container.
internal var transitionContainer: UIView?
/// An optional completion callback.
internal var completionCallback: ((Bool) -> Void)?
/// Binds the render cycle to the transition animation.
internal var displayLink: CADisplayLink?
/// An Array of observers that are updated during a transition.
internal var transitionObservers: [MotionTransitionObserver]?
/// Max duration used by MotionAnimators and MotionPlugins.
public internal(set) var totalDuration: TimeInterval = 0
/// The currently running animation duration.
internal var currentAnimationDuration: TimeInterval = 0
/// The start time of the animation.
internal var beginTime: TimeInterval? {
didSet {
guard nil != beginTime else {
displayLink?.isPaused = true
displayLink?.remove(from: RunLoop.main, forMode: RunLoopMode(rawValue: RunLoopMode.commonModes.rawValue))
displayLink = nil
return
}
guard nil == displayLink else {
return
}
displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink?.add(to: RunLoop.main, forMode: RunLoopMode(rawValue: RunLoopMode.commonModes.rawValue))
}
}
/// A boolean indicating if the transition has finished.
internal var isFinished = true
/// An Array of MotionPreprocessors used during a transition.
internal fileprivate(set) lazy var preprocessors = [MotionPreprocessor]()
/// An Array of MotionAnimators used during a transition.
internal fileprivate(set) lazy var animators = [MotionAnimator]()
/// An Array of MotionPlugins used during a transition.
internal fileprivate(set) lazy var plugins = [MotionPlugin]()
/// The matching fromViews to toViews based on the motionIdentifier value.
internal fileprivate(set) lazy var transitionPairs = [(fromViews: [UIView], toViews: [UIView])]()
/// Plugins that are enabled during the transition.
internal static var enabledPlugins = [MotionPlugin.Type]()
/// Initializer.
internal override init() {}
}
public extension MotionController {
/**
Receive callbacks on each animation frame.
Observers will be cleaned when a transition completes.
- Parameter observer: A MotionTransitionObserver.
*/
func addTransitionObserver(observer: MotionTransitionObserver) {
if nil == transitionObservers {
transitionObservers = []
}
transitionObservers?.append(observer)
}
}
fileprivate extension MotionController {
/// Updates the transition observers.
func updateTransitionObservers() {
guard let observers = transitionObservers else {
return
}
for v in observers {
v.motion(transitionObserver: v, didUpdateWith: elapsedTime)
}
}
/// Updates the animators.
func updateAnimators() {
let t = elapsedTime * totalDuration
for a in animators {
a.seek(to: t)
}
}
/// Updates the plugins.
func updatePlugins() {
let t = elapsedTime * totalDuration
for p in plugins where p.requirePerFrameCallback {
p.seek(to: t)
}
}
}
fileprivate extension MotionController {
/**
Handler for the DisplayLink updates.
- Parameter _ link: CADisplayLink.
*/
@objc
func handleDisplayLink(_ link: CADisplayLink) {
guard isTransitioning else {
return
}
guard 0 < currentAnimationDuration else {
return
}
guard let t = beginTime else {
return
}
let cTime = CACurrentMediaTime() - t
if cTime > currentAnimationDuration {
elapsedTime = isFinished ? 1 : 0
beginTime = nil
complete(isFinished: isFinished)
} else {
var eTime = cTime / totalDuration
if !isFinished {
eTime = 1 - eTime
}
elapsedTime = max(0, min(1, eTime))
}
}
}
public extension MotionController {
/**
Updates the elapsed time for the interactive transition.
- Parameter elapsedTime t: the current progress, must be between -1...1.
*/
public func update(elapsedTime t: TimeInterval) {
guard isTransitioning else {
return
}
beginTime = nil
elapsedTime = max(-1, min(1, t))
}
/**
Finish the interactive transition.
Will stop the interactive transition and animate from the
current state to the **end** state
- Parameter isAnimated: A boolean indicating if the completion is animated.
*/
public func end(isAnimated: Bool = true) {
guard isTransitioning else {
return
}
guard isAnimated else {
complete(isFinished: true)
return
}
var t: TimeInterval = 0
for a in animators {
t = max(t, a.resume(at: elapsedTime * totalDuration, isReversed: false))
}
complete(after: t, isFinished: true)
}
/**
Cancel the interactive transition.
Will stop the interactive transition and animate from the
current state to the **begining** state
- Parameter isAnimated: A boolean indicating if the completion is animated.
*/
public func cancel(isAnimated: Bool = true) {
guard isTransitioning else {
return
}
guard isAnimated else {
complete(isFinished: false)
return
}
var d: TimeInterval = 0
for a in animators {
var t = elapsedTime
if t < 0 {
t = -t
}
d = max(d, a.resume(at: t * totalDuration, isReversed: true))
}
complete(after: d, isFinished: false)
}
/**
Override transition animations during an interactive animation.
For example:
Motion.shared.apply([.position(x:50, y:50)], to: view)
will set the view's position to 50, 50
- Parameter transitions: An Array of MotionTransitions.
- Parameter to view: A UIView.
*/
public func apply(transitions: [MotionTransition], to view: UIView) {
guard isTransitioning else {
return
}
let s = MotionTransitionState(transitions: transitions)
let v = context.transitionPairedView(for: view) ?? view
for a in animators {
a.apply(state: s, to: v)
}
}
}
internal extension MotionController {
/**
Load plugins, processors, animators, container, & context
The transitionContainer must already be set.
Subclasses should call context.set(fromViews: toViews) after
inserting fromViews & toViews into the container
*/
@objc
func prepareTransition() {
guard isTransitioning else {
return
}
prepareTransitionContainer()
prepareContext()
preparePreprocessors()
prepareAnimators()
preparePlugins()
}
/// Prepares the transition fromView & toView pairs.
@objc
func prepareTransitionPairs() {
guard isTransitioning else {
return
}
for a in animators {
let fv = context.fromViews.filter { (view: UIView) -> Bool in
return a.canAnimate(view: view, isAppearing: false)
}
let tv = context.toViews.filter {
return a.canAnimate(view: $0, isAppearing: true)
}
transitionPairs.append((fv, tv))
}
}
}
internal extension MotionController {
/// Executes the preprocessors' process function.
func processContext() {
guard isTransitioning else {
return
}
for x in preprocessors {
x.process(fromViews: context.fromViews, toViews: context.toViews)
}
}
/**
Animates the views. Subclasses should call `prepareTransition` &
`prepareTransitionPairs` before calling `animate`.
*/
@objc
func animate() {
guard isTransitioning else {
return
}
for (fv, tv) in transitionPairs {
for view in fv {
context.hide(view: view)
}
for view in tv {
context.hide(view: view)
}
}
var t: TimeInterval = 0
var b = false
for (i, a) in animators.enumerated() {
let d = a.animate(fromViews: transitionPairs[i].0, toViews: transitionPairs[i].1)
if .infinity == d {
b = true
} else {
t = max(t, d)
}
}
totalDuration = t
if b {
update(elapsedTime: 0)
} else {
complete(after: t, isFinished: true)
}
}
/**
Complete the transition.
- Parameter after: A TimeInterval.
- Parameter isFinished: A Boolean indicating if the transition
has completed.
*/
func complete(after: TimeInterval, isFinished: Bool) {
guard isTransitioning else {
return
}
if after <= 0.001 {
complete(isFinished: isFinished)
return
}
let v = (isFinished ? elapsedTime : 1 - elapsedTime) * totalDuration
self.isFinished = isFinished
currentAnimationDuration = after + v
beginTime = CACurrentMediaTime() - v
}
/**
Complete the transition.
- Parameter isFinished: A Boolean indicating if the transition
has completed.
*/
@objc
func complete(isFinished: Bool) {
guard isTransitioning else {
return
}
for a in animators {
a.clean()
}
transitionContainer?.isUserInteractionEnabled = true
let completion = completionCallback
transitionObservers = nil
transitionContainer = nil
completionCallback = nil
container = nil
context = nil
beginTime = nil
elapsedTime = 0
totalDuration = 0
preprocessors.removeAll()
animators.removeAll()
plugins.removeAll()
transitionPairs.removeAll()
completion?(isFinished)
}
}
fileprivate extension MotionController {
/// Prepares the transition container.
func prepareTransitionContainer() {
guard let v = transitionContainer else {
return
}
v.isUserInteractionEnabled = false
// a view to hold all the animating views
container = UIView(frame: v.bounds)
v.addSubview(container!)
}
/// Prepares the context.
func prepareContext() {
guard let v = container else {
return
}
context = MotionContext(container: v)
}
/// Prepares the preprocessors.
func preparePreprocessors() {
for x in [
IgnoreSubviewTransitionsPreprocessor(),
MatchPreprocessor(),
SourcePreprocessor(),
CascadePreprocessor(),
DurationPreprocessor()] as [MotionPreprocessor] {
preprocessors.append(x)
}
for x in preprocessors {
x.context = context
}
}
/// Prepares the animators.
func prepareAnimators() {
animators.append(MotionTransitionAnimator<MotionCoreAnimationViewContext>())
if #available(iOS 10, tvOS 10, *) {
animators.append(MotionTransitionAnimator<MotionViewPropertyViewContext>())
}
for v in animators {
v.context = context
}
}
/// Prepares the plugins.
func preparePlugins() {
for x in Motion.enabledPlugins.map({
return $0.init()
}) {
plugins.append(x)
}
for plugin in plugins {
preprocessors.append(plugin)
animators.append(plugin)
}
}
}
internal extension MotionController {
/**
Checks if a given plugin is enabled.
- Parameter plugin: A MotionPlugin.Type.
- Returns: A boolean indicating if the plugin is enabled or not.
*/
static func isEnabled(plugin: MotionPlugin.Type) -> Bool {
return nil != enabledPlugins.index(where: { return $0 == plugin })
}
/**
Enables a given plugin.
- Parameter plugin: A MotionPlugin.Type.
*/
static func enable(plugin: MotionPlugin.Type) {
disable(plugin: plugin)
enabledPlugins.append(plugin)
}
/**
Disables a given plugin.
- Parameter plugin: A MotionPlugin.Type.
*/
static func disable(plugin: MotionPlugin.Type) {
guard let index = enabledPlugins.index(where: { return $0 == plugin }) else {
return
}
enabledPlugins.remove(at: index)
}
}
internal extension MotionController {
// should call this after `prepareTransitionPairs` & before `processContext`
func insert<T>(preprocessor: MotionPreprocessor, before: T.Type) {
let i = preprocessors.index { $0 is T } ?? preprocessors.count
preprocessor.context = context
preprocessors.insert(preprocessor, at: i)
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
protocol MotionProgressRunnerDelegate: class {
func update(elapsedTime: TimeInterval)
func complete(isFinished: Bool)
}
class MotionProgressRunner {
weak var delegate: MotionProgressRunnerDelegate?
var isRunning: Bool {
return displayLink != nil
}
internal var timePassed: TimeInterval = 0
internal var duration: TimeInterval = 0
internal var displayLink: CADisplayLink?
internal var isReversed: Bool = false
@objc
func displayUpdate(_ link: CADisplayLink) {
timePassed += isReversed ? -link.duration : link.duration
if isReversed, timePassed <= 1.0 / 120 {
delegate?.complete(isFinished: false)
stop()
return
}
if !isReversed, timePassed > duration - 1.0 / 120 {
delegate?.complete(isFinished: true)
stop()
return
}
delegate?.update(elapsedTime: timePassed / duration)
}
func start(timePassed: TimeInterval, totalTime: TimeInterval, reverse: Bool) {
stop()
self.timePassed = timePassed
self.isReversed = reverse
self.duration = totalTime
displayLink = CADisplayLink(target: self, selector: #selector(displayUpdate(_:)))
displayLink!.add(to: RunLoop.main, forMode: RunLoopMode(rawValue: RunLoopMode.commonModes.rawValue))
}
func stop() {
displayLink?.isPaused = true
displayLink?.remove(from: RunLoop.main, forMode: RunLoopMode(rawValue: RunLoopMode.commonModes.rawValue))
displayLink = nil
}
}
......@@ -26,42 +26,23 @@
* THE SOFTWARE.
*/
import UIKit
public class MotionIndependentController: MotionController {
/// An initializer.
public override init() {
super.init()
}
/**
Transitions source views to their corresponding destination view
within a given root view.
- Parameter rootView: A UIView.
- Parameter fromViews: An Array of UIViews.
- Parameter toViews: An Array of UIViews.
- Parameter completion: An optional callback.
*/
public func transition(rootView: UIView, fromViews: [UIView], toViews: [UIView], completion: ((Bool) -> Void)? = nil) {
transitionContainer = rootView
completionCallback = completion
prepareTransition()
prepareContext(fromViews: fromViews, toViews: toViews)
prepareTransitionPairs()
animate()
}
@objc(MotionState)
public enum MotionState: Int {
/// Motion is able to start a new transition.
case possible
/// UIKit has notified Motion about a pending transition.
/// Motion hasn't started preparation.
case notified
/// Motion's `start` method has been called. Preparing the animation.
case starting
/// Motions's `animate` method has been called. Animation has started.
case animating
/// Motions's `complete` method has been called. Transition has ended or has
/// been cancelled. Motion is cleaning up.
case completing
}
fileprivate extension MotionIndependentController {
/**
Prepares the context.
- Parameter fromViews: An Array of UIViews.
- PArameter toViews: An Array of UIViews.
*/
func prepareContext(fromViews: [UIView], toViews: [UIView]) {
context.set(fromViews: fromViews, toViews: toViews)
processContext()
}
}
......@@ -218,7 +218,7 @@ class TransitionPreprocessor: MotionPreprocessor {
if case .auto = defaultAnimation {
if isNavigationController, let navAnim = toViewController?.navigationController?.motionNavigationTransitionType {
defaultAnimation = navAnim
} else if isTabBarController, let tabAnim = toViewController?.tabBarController?.motionTabBarTransitionType {
defaultAnimation = tabAnim
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment