Commit c35de387 by Daniel Dahan

updated MotionTransition to Motion

parent 9f0421f4
......@@ -14,9 +14,7 @@
961409B81E43D21300E7BA99 /* Motion.h in Headers */ = {isa = PBXBuildFile; fileRef = 96C98DED1E438A5700B22906 /* Motion.h */; settings = {ATTRIBUTES = (Public, ); }; };
96C98DE41E4382B100B22906 /* Motion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C98DE31E4382B100B22906 /* Motion.swift */; };
96C98DE61E43848500B22906 /* MotionAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C98DE51E43848500B22906 /* MotionAnimation.swift */; };
96C98DE81E43849B00B22906 /* MotionTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C98DE71E43849B00B22906 /* MotionTransition.swift */; };
96C98DEB1E4389BE00B22906 /* MotionAnimation.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96C98DE51E43848500B22906 /* MotionAnimation.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96C98DEC1E4389BE00B22906 /* MotionTransition.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96C98DE71E43849B00B22906 /* MotionTransition.swift */; settings = {ATTRIBUTES = (Public, ); }; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
......@@ -27,7 +25,6 @@
96C98DE21E43809D00B22906 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
96C98DE31E4382B100B22906 /* Motion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Motion.swift; sourceTree = "<group>"; };
96C98DE51E43848500B22906 /* MotionAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimation.swift; sourceTree = "<group>"; };
96C98DE71E43849B00B22906 /* MotionTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionTransition.swift; sourceTree = "<group>"; };
96C98DED1E438A5700B22906 /* Motion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Motion.h; sourceTree = "<group>"; };
/* End PBXFileReference section */
......@@ -66,7 +63,6 @@
96C98DED1E438A5700B22906 /* Motion.h */,
96C98DE31E4382B100B22906 /* Motion.swift */,
96C98DE51E43848500B22906 /* MotionAnimation.swift */,
96C98DE71E43849B00B22906 /* MotionTransition.swift */,
961409A91E43CF1B00E7BA99 /* Motion+Obj-C.swift */,
961409AD1E43CFFD00E7BA99 /* Motion+UIView.swift */,
);
......@@ -84,7 +80,6 @@
961409B61E43D17200E7BA99 /* Motion+Obj-C.swift in Headers */,
961409B71E43D17200E7BA99 /* Motion+UIView.swift in Headers */,
96C98DEB1E4389BE00B22906 /* MotionAnimation.swift in Headers */,
96C98DEC1E4389BE00B22906 /* MotionTransition.swift in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
......@@ -161,7 +156,6 @@
96C98DE61E43848500B22906 /* MotionAnimation.swift in Sources */,
961409AA1E43CF1B00E7BA99 /* Motion+Obj-C.swift in Sources */,
96C98DE41E4382B100B22906 /* Motion.swift in Sources */,
96C98DE81E43849B00B22906 /* MotionTransition.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
......
......@@ -86,7 +86,275 @@ public func MotionAnimationTimingFunctionToValue(timingFunction: MotionAnimation
public typealias MotionDelayCancelBlock = (Bool) -> Void
public struct Motion {
fileprivate var MotionInstanceKey: UInt8 = 0
fileprivate var MotionInstanceControllerKey: UInt8 = 0
fileprivate struct MotionInstance {
fileprivate var identifier: String
fileprivate var animations: [MotionAnimation]
}
fileprivate struct MotionInstanceController {
fileprivate var isEnabled: Bool
fileprivate weak var delegate: MotionDelegate?
}
extension UIViewController: MotionDelegate, UIViewControllerTransitioningDelegate {
/// MotionInstanceController Reference.
fileprivate var motionInstanceController: MotionInstanceController {
get {
return AssociatedObject(base: self, key: &MotionInstanceControllerKey) {
return MotionInstanceController(isEnabled: false, delegate: nil)
}
}
set(value) {
AssociateObject(base: self, key: &MotionInstanceControllerKey, value: value)
}
}
open var isMotionEnabled: Bool {
get {
return motionInstanceController.isEnabled
}
set(value) {
if value {
modalPresentationStyle = .custom
transitioningDelegate = self
motionDelegate = self
}
motionInstanceController.isEnabled = value
}
}
open weak var motionDelegate: MotionDelegate? {
get {
return motionInstanceController.delegate
}
set(value) {
motionInstanceController.delegate = value
}
}
}
extension UIViewController {
open func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return isMotionEnabled ? Motion(isPresenting: true) : nil
}
open func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return isMotionEnabled ? Motion() : nil
}
open func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return isMotionEnabled ? MotionPresentationController(presentedViewController: presented, presenting: presenting) : nil
}
}
extension UIView {
/// MaterialTransitionItem Reference.
fileprivate var motionInstance: MotionInstance {
get {
return AssociatedObject(base: self, key: &MotionInstanceKey) {
return MotionInstance(identifier: "", animations: [])
}
}
set(value) {
AssociateObject(base: self, key: &MotionInstanceKey, value: value)
}
}
open var motionIdentifier: String {
get {
return motionInstance.identifier
}
set(value) {
motionInstance.identifier = value
}
}
open var motionAnimations: [MotionAnimation] {
get {
return motionInstance.animations
}
set(value) {
motionInstance.animations = value
}
}
open func transitionSnapshot(afterUpdates: Bool, shouldHide: Bool = true) -> UIView {
isHidden = false
let oldCornerRadius = cornerRadius
cornerRadius = 0
var oldBackgroundColor: UIColor?
if shouldHide {
oldBackgroundColor = backgroundColor
backgroundColor = .clear
}
let oldTransform = motionTransform
motionTransform = CATransform3DIdentity
let v = snapshotView(afterScreenUpdates: afterUpdates)!
cornerRadius = oldCornerRadius
if shouldHide {
backgroundColor = oldBackgroundColor
}
motionTransform = oldTransform
let contentView = v.subviews.first!
contentView.cornerRadius = cornerRadius
contentView.masksToBounds = true
v.motionIdentifier = motionIdentifier
v.position = motionPosition
v.bounds = bounds
v.cornerRadius = cornerRadius
v.zPosition = zPosition
v.opacity = opacity
v.isOpaque = isOpaque
v.anchorPoint = anchorPoint
v.masksToBounds = masksToBounds
v.borderColor = borderColor
v.borderWidth = borderWidth
v.shadowRadius = shadowRadius
v.shadowOpacity = shadowOpacity
v.shadowColor = shadowColor
v.shadowOffset = shadowOffset
v.contentMode = contentMode
v.motionTransform = motionTransform
v.backgroundColor = backgroundColor
isHidden = shouldHide
return v
}
}
open class MotionPresentationController: UIPresentationController {
open override func presentationTransitionWillBegin() {
guard nil != containerView else {
return
}
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
print("Animating")
})
print("presentationTransitionWillBegin")
}
open override func presentationTransitionDidEnd(_ completed: Bool) {
print("presentationTransitionDidEnd")
}
open override func dismissalTransitionWillBegin() {
guard nil != containerView else {
return
}
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
print("Animating")
})
print("dismissalTransitionWillBegin")
}
open override func dismissalTransitionDidEnd(_ completed: Bool) {
print("dismissalTransitionDidEnd")
}
open override var frameOfPresentedViewInContainerView: CGRect {
return containerView?.bounds ?? .zero
}
}
@objc(MotionDelegate)
public protocol MotionDelegate {
@objc
optional func motion(motion: Motion, willTransition fromView: UIView, toView: UIView)
@objc
optional func motion(motion: Motion, didTransition fromView: UIView, toView: UIView)
@objc
optional func motionModifyDelay(motion: Motion) -> TimeInterval
@objc
optional func motionTransitionAnimation(motion: Motion)
}
open class Motion: NSObject {
open var isPresenting: Bool
open fileprivate(set) var transitionPairs = [(UIView, UIView)]()
open var transitionSnapshot: UIView!
open let transitionBackgroundView = UIView()
open var toViewController: UIViewController!
open var fromViewController: UIViewController!
open var transitionContext: UIViewControllerContextTransitioning!
open var delay: TimeInterval = 0
open var duration: TimeInterval = 0.35
open var containerView: UIView!
open var transitionView = UIView()
fileprivate var modifiedDelay: TimeInterval {
return fromViewController?.motionDelegate?.motionModifyDelay?(motion: self) ?? 0
}
public override init() {
isPresenting = false
super.init()
}
public init(isPresenting: Bool) {
self.isPresenting = isPresenting
super.init()
}
open var toView: UIView {
return toViewController.view
}
open var toSubviews: [UIView] {
return Motion.subviews(of: toView)
}
open var fromView: UIView {
return fromViewController.view
}
open var fromSubviews: [UIView] {
return Motion.subviews(of: fromView)
}
open class func subviews(of view: UIView) -> [UIView] {
var views: [UIView] = []
Motion.subviews(of: view, views: &views)
return views
}
open class func subviews(of view: UIView, views: inout [UIView]) {
for v in view.subviews {
if 0 < v.motionIdentifier.utf16.count {
views.append(v)
}
subviews(of: v, views: &views)
}
}
/**
Executes a block of code after a time delay.
- Parameter duration: An animation duration time.
......@@ -95,8 +363,7 @@ public struct Motion {
the animations have completed.
*/
@discardableResult
public static func delay(_ time: TimeInterval, execute block: @escaping () -> Void) -> MotionDelayCancelBlock? {
open class func delay(_ time: TimeInterval, execute block: @escaping () -> Void) -> MotionDelayCancelBlock? {
func asyncAfter(completion: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + time, execute: completion)
}
......@@ -124,35 +391,168 @@ public struct Motion {
Cancels the delayed MotionDelayCancelBlock.
- Parameter delayed completion: An MotionDelayCancelBlock.
*/
public static func cancel(delayed completion: MotionDelayCancelBlock) {
open class func cancel(delayed completion: MotionDelayCancelBlock) {
completion(true)
}
}
/**
Disables the default animations set on CALayers.
- Parameter animations: A callback that wraps the animations to disable.
*/
public static func disable(animations: (() -> Void)) {
animate(duration: 0, animations: animations)
extension Motion: UIViewControllerAnimatedTransitioning {
@objc(animateTransition:)
open func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
prepareToViewController()
prepareFromViewController()
fromViewController.motionDelegate?.motion?(motion: self, willTransition: fromView, toView: toView)
Motion.delay(modifiedDelay) { [weak self] in
guard let s = self else {
return
}
/**
Runs an animation with a specified duration.
- Parameter duration: An animation duration time.
- Parameter animations: An animation block.
- Parameter timingFunction: An MotionAnimationTimingFunction value.
- Parameter completion: A completion block that is executed once
the animations have completed.
*/
public static func animate(duration: CFTimeInterval, timingFunction: MotionAnimationTimingFunction = .easeInEaseOut, animations: (() -> Void), completion: (() -> Void)? = nil) {
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
CATransaction.setCompletionBlock(completion)
CATransaction.setAnimationTimingFunction(MotionAnimationTimingFunctionToValue(timingFunction: timingFunction))
animations()
CATransaction.commit()
s.prepareContainerView()
s.prepareTransitionSnapshot()
s.prepareTransitionPairs()
s.prepareTransitionView()
s.prepareTransitionBackgroundView()
s.prepareTransitionToView()
s.prepareTransitionAnimation()
}
}
@objc(transitionDuration:)
open func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return delay + duration
}
}
extension Motion {
fileprivate func prepareToViewController() {
guard let v = transitionContext.viewController(forKey: .to) else {
return
}
toViewController = v
}
fileprivate func prepareFromViewController() {
guard let v = transitionContext.viewController(forKey: .from) else {
return
}
fromViewController = v
}
fileprivate func prepareContainerView() {
containerView = transitionContext.containerView
}
fileprivate func prepareTransitionSnapshot() {
transitionSnapshot = fromView.transitionSnapshot(afterUpdates: true, shouldHide: false)
transitionSnapshot.frame = containerView.bounds
containerView.insertSubview(transitionSnapshot, aboveSubview: fromView)
}
fileprivate func prepareTransitionPairs() {
for from in fromSubviews {
for to in toSubviews {
guard to.motionIdentifier == from.motionIdentifier else {
continue
}
transitionPairs.append((from, to))
}
}
}
fileprivate func prepareTransitionView() {
transitionView.frame = containerView.bounds
transitionView.isUserInteractionEnabled = false
containerView.insertSubview(transitionView, belowSubview: transitionSnapshot)
}
fileprivate func prepareTransitionBackgroundView() {
transitionBackgroundView.backgroundColor = isPresenting ? .clear : fromView.backgroundColor ?? .clear
transitionBackgroundView.frame = transitionView.bounds
transitionView.addSubview(transitionBackgroundView)
}
fileprivate func prepareTransitionToView() {
toView.isHidden = isPresenting
containerView.insertSubview(toView, belowSubview: transitionView)
toView.updateConstraints()
toView.setNeedsLayout()
toView.layoutIfNeeded()
}
fileprivate func prepareTransitionAnimation() {
addTransitionAnimations()
addBackgroundMotionAnimation()
cleanupAnimation()
removeTransitionSnapshot()
}
}
extension Motion {
fileprivate func addTransitionAnimations() {
for (from, to) in transitionPairs {
var snapshotAnimations = [CABasicAnimation]()
var snapshotChildAnimations = [CABasicAnimation]()
let sizeAnimation = Motion.size(to.bounds.size)
let cornerRadiusAnimation = Motion.corner(radius: to.cornerRadius)
snapshotAnimations.append(sizeAnimation)
snapshotAnimations.append(cornerRadiusAnimation)
snapshotAnimations.append(Motion.position(to: to.motionPosition))
snapshotAnimations.append(Motion.transform(transform: to.motionTransform))
snapshotAnimations.append(Motion.background(color: to.backgroundColor ?? .clear))
snapshotChildAnimations.append(cornerRadiusAnimation)
snapshotChildAnimations.append(sizeAnimation)
snapshotChildAnimations.append(Motion.position(x: to.bounds.width / 2, y: to.bounds.height / 2))
let d = transitionDuration(animations: to.motionAnimations)
let snapshot = from.transitionSnapshot(afterUpdates: true)
transitionView.addSubview(snapshot)
Motion.delay(motionDelay(animations: to.motionAnimations)) { [weak self, weak to] in
guard let s = self else {
return
}
guard let v = to else {
return
}
let tf = s.motionTimingFunction(animations: v.motionAnimations)
let snapshotGroup = Motion.animate(group: snapshotAnimations, duration: d)
snapshotGroup.fillMode = MotionAnimationFillModeToValue(mode: .forwards)
snapshotGroup.isRemovedOnCompletion = false
snapshotGroup.timingFunction = MotionAnimationTimingFunctionToValue(timingFunction: tf)
let snapshotChildGroup = Motion.animate(group: snapshotChildAnimations, duration: d)
snapshotChildGroup.fillMode = MotionAnimationFillModeToValue(mode: .forwards)
snapshotChildGroup.isRemovedOnCompletion = false
snapshotChildGroup.timingFunction = MotionAnimationTimingFunctionToValue(timingFunction: tf)
snapshot.animate(snapshotGroup)
snapshot.subviews.first!.animate(snapshotChildGroup)
}
}
fromViewController.motionDelegate?.motionTransitionAnimation?(motion: self)
toViewController.motionDelegate?.motionTransitionAnimation?(motion: self)
}
fileprivate func addBackgroundMotionAnimation() {
transitionBackgroundView.motion(.backgroundColor(isPresenting ? toView.backgroundColor ?? .clear : .clear), .duration(transitionDuration(using: transitionContext)))
}
}
extension Motion {
/**
Creates a CAAnimationGroup.
- Parameter animations: An Array of CAAnimation objects.
......@@ -160,7 +560,7 @@ public struct Motion {
- Parameter duration: An animation duration time for the group.
- Returns: A CAAnimationGroup.
*/
public static func animate(group animations: [CAAnimation], timingFunction: MotionAnimationTimingFunction = .easeInEaseOut, duration: CFTimeInterval = 0.5) -> CAAnimationGroup {
internal class func animate(group animations: [CAAnimation], timingFunction: MotionAnimationTimingFunction = .easeInEaseOut, duration: CFTimeInterval = 0.5) -> CAAnimationGroup {
let group = CAAnimationGroup()
group.fillMode = MotionAnimationFillModeToValue(mode: .forwards)
group.isRemovedOnCompletion = false
......@@ -169,19 +569,92 @@ public struct Motion {
group.timingFunction = MotionAnimationTimingFunctionToValue(timingFunction: timingFunction)
return group
}
}
/**
Executes an animation block with a given delay and duration.
- Parameter delay time: A CFTimeInterval.
- Parameter duration: An animation duration time.
- Parameter animations: An animation block.
- Parameter completion: A completion block that is executed once
the animations have completed.
*/
public static func animate(delay time: CFTimeInterval, duration: CFTimeInterval, animations: @escaping (() -> Void), completion: (() -> Void)? = nil) {
delay(time) {
animate(duration: duration, animations: animations, completion: completion)
extension Motion {
fileprivate func motionDelay(animations: [MotionAnimation]) -> TimeInterval {
var t: TimeInterval = 0
for a in animations {
switch a {
case let .delay(time):
if time > delay {
delay = time
}
t = time
default:break
}
}
return t
}
fileprivate func transitionDuration(animations: [MotionAnimation]) -> TimeInterval {
var t: TimeInterval = 0.35
for a in animations {
switch a {
case let .duration(time):
if time > duration {
duration = time
}
t = time
default:break
}
}
return t
}
fileprivate func motionTimingFunction(animations: [MotionAnimation]) -> MotionAnimationTimingFunction {
var t = MotionAnimationTimingFunction.easeInEaseOut
for a in animations {
switch a {
case let .timingFunction(timingFunction):
t = timingFunction
default:break
}
}
return t
}
}
extension Motion {
fileprivate func cleanupAnimation() {
Motion.delay(transitionDuration(using: transitionContext) + modifiedDelay) { [weak self] in
guard let s = self else {
return
}
s.showToSubviews()
s.clearTransitionView()
s.clearTransitionPairs()
s.completeTransition()
}
}
fileprivate func removeTransitionSnapshot() {
Motion.delay(delay) { [weak self] in
self?.transitionSnapshot.removeFromSuperview()
}
}
fileprivate func showToSubviews() {
toSubviews.forEach {
$0.isHidden = false
}
toView.isHidden = false
}
fileprivate func clearTransitionPairs() {
transitionPairs.removeAll()
}
fileprivate func clearTransitionView() {
transitionView.removeFromSuperview()
transitionView.subviews.forEach {
$0.removeFromSuperview()
}
}
fileprivate func completeTransition() {
toViewController.motionDelegate?.motion?(motion: self, didTransition: fromView, toView: toView)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
......@@ -32,6 +32,7 @@ import UIKit
public enum MotionAnimationKeyPath: String {
case backgroundColor
case barTintColor
case cornerRadius
case transform
case rotation = "transform.rotation"
......@@ -61,6 +62,7 @@ public enum MotionAnimation {
case duration(TimeInterval)
case custom(CABasicAnimation)
case backgroundColor(UIColor)
case barTintColor(UIColor)
case cornerRadius(CGFloat)
case transform(CATransform3D)
case rotationAngle(CGFloat)
......@@ -243,6 +245,8 @@ extension CALayer {
a.append(animation)
case let .backgroundColor(color):
a.append(Motion.background(color: color))
case let .barTintColor(color):
a.append(Motion.barTint(color: color))
case let .cornerRadius(radius):
a.append(Motion.corner(radius: radius))
case let .transform(transform):
......@@ -410,6 +414,17 @@ extension Motion {
}
/**
Creates a CABasicAnimation for the barTintColor key path.
- Parameter color: A UIColor.
- Returns: A CABasicAnimation.
*/
public static func barTint(color: UIColor) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: .barTintColor)
animation.toValue = color.cgColor
return animation
}
/**
Creates a CABasicAnimation for the cornerRadius key path.
- Parameter radius: A CGFloat.
- Returns: A CABasicAnimation.
......
/*
* Copyright (C) 2015 - 2017, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of CosmicMind nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import UIKit
fileprivate var MotionInstanceKey: UInt8 = 0
fileprivate var MotionInstanceControllerKey: UInt8 = 0
fileprivate struct MotionInstance {
fileprivate var identifier: String
fileprivate var animations: [MotionAnimation]
}
fileprivate struct MotionInstanceController {
fileprivate var isEnabled: Bool
fileprivate weak var delegate: MotionDelegate?
}
extension UIViewController: UIViewControllerTransitioningDelegate {
/// MotionInstanceController Reference.
fileprivate var motionInstanceController: MotionInstanceController {
get {
return AssociatedObject(base: self, key: &MotionInstanceControllerKey) {
return MotionInstanceController(isEnabled: false, delegate: nil)
}
}
set(value) {
AssociateObject(base: self, key: &MotionInstanceControllerKey, value: value)
}
}
open var isMotionEnabled: Bool {
get {
return motionInstanceController.isEnabled
}
set(value) {
if value {
modalPresentationStyle = .custom
transitioningDelegate = self
}
motionInstanceController.isEnabled = value
}
}
open weak var motionDelegate: MotionDelegate? {
get {
return motionInstanceController.delegate
}
set(value) {
motionInstanceController.delegate = value
}
}
}
extension UIViewController {
open func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return isMotionEnabled ? MotionTransition(isPresenting: true) : nil
}
open func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return isMotionEnabled ? MotionTransition() : nil
}
open func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return isMotionEnabled ? MotionTransitionPresentationController(presentedViewController: presented, presenting: presenting) : nil
}
}
extension UIView {
/// MaterialTransitionItem Reference.
fileprivate var motionInstance: MotionInstance {
get {
return AssociatedObject(base: self, key: &MotionInstanceKey) {
return MotionInstance(identifier: "", animations: [])
}
}
set(value) {
AssociateObject(base: self, key: &MotionInstanceKey, value: value)
}
}
open var motionIdentifier: String {
get {
return motionInstance.identifier
}
set(value) {
motionInstance.identifier = value
}
}
open var motionAnimations: [MotionAnimation] {
get {
return motionInstance.animations
}
set(value) {
motionInstance.animations = value
}
}
open func motionSnapshot(afterUpdates: Bool, shouldHide: Bool = true) -> UIView {
isHidden = false
let oldCornerRadius = cornerRadius
cornerRadius = 0
var oldBackgroundColor: UIColor?
if shouldHide {
oldBackgroundColor = backgroundColor
backgroundColor = .clear
}
let oldTransform = motionTransform
motionTransform = CATransform3DIdentity
let v = snapshotView(afterScreenUpdates: afterUpdates)!
cornerRadius = oldCornerRadius
if shouldHide {
backgroundColor = oldBackgroundColor
}
motionTransform = oldTransform
let contentView = v.subviews.first!
contentView.cornerRadius = cornerRadius
contentView.masksToBounds = true
v.motionIdentifier = motionIdentifier
v.position = motionPosition
v.bounds = bounds
v.cornerRadius = cornerRadius
v.zPosition = zPosition
v.opacity = opacity
v.isOpaque = isOpaque
v.anchorPoint = anchorPoint
v.masksToBounds = masksToBounds
v.borderColor = borderColor
v.borderWidth = borderWidth
v.shadowRadius = shadowRadius
v.shadowOpacity = shadowOpacity
v.shadowColor = shadowColor
v.shadowOffset = shadowOffset
v.contentMode = contentMode
v.motionTransform = motionTransform
v.backgroundColor = backgroundColor
isHidden = shouldHide
return v
}
}
open class MotionTransitionPresentationController: UIPresentationController {
open override func presentationTransitionWillBegin() {
guard nil != containerView else {
return
}
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
print("Animating")
})
print("presentationTransitionWillBegin")
}
open override func presentationTransitionDidEnd(_ completed: Bool) {
print("presentationTransitionDidEnd")
}
open override func dismissalTransitionWillBegin() {
guard nil != containerView else {
return
}
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
print("Animating")
})
print("dismissalTransitionWillBegin")
}
open override func dismissalTransitionDidEnd(_ completed: Bool) {
print("dismissalTransitionDidEnd")
}
open override var frameOfPresentedViewInContainerView: CGRect {
return containerView?.bounds ?? .zero
}
}
@objc(MotionDelegate)
public protocol MotionDelegate {
@objc
optional func motion(transition: MotionTransition, willTransition fromView: UIView, toView: UIView)
@objc
optional func motion(transition: MotionTransition, didTransition fromView: UIView, toView: UIView)
@objc
optional func motionModifyDelay(transition: MotionTransition) -> TimeInterval
}
open class MotionTransition: NSObject {
open var isPresenting: Bool
open fileprivate(set) var transitionPairs = [(UIView, UIView)]()
open var transitionSnapshot: UIView!
open let transitionBackgroundView = UIView()
open var toViewController: UIViewController!
open var fromViewController: UIViewController!
open var transitionContext: UIViewControllerContextTransitioning!
open var delay: TimeInterval = 0
open var duration: TimeInterval = 0.35
open var containerView: UIView!
open var transitionView = UIView()
fileprivate var modifiedDelay: TimeInterval {
return fromViewController?.motionDelegate?.motionModifyDelay?(transition: self) ?? 0
}
public override init() {
isPresenting = false
super.init()
}
public init(isPresenting: Bool) {
self.isPresenting = isPresenting
super.init()
}
open var toView: UIView {
return toViewController.view
}
open var toSubviews: [UIView] {
return subviews(of: toView)
}
open var fromView: UIView {
return fromViewController.view
}
open var fromSubviews: [UIView] {
return subviews(of: fromView)
}
open func subviews(of view: UIView) -> [UIView] {
var views: [UIView] = []
subviews(of: view, views: &views)
return views
}
open func subviews(of view: UIView, views: inout [UIView]) {
for v in view.subviews {
if 0 < v.motionIdentifier.utf16.count {
views.append(v)
}
subviews(of: v, views: &views)
}
}
}
extension UINavigationController: UINavigationControllerDelegate {
open override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
@objc(navigationController:animationControllerForOperation:fromViewController:toViewController:)
open func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return isMotionEnabled ? MotionTransition(isPresenting: operation == .push) : nil
}
}
extension UITabBarController: UITabBarControllerDelegate {
open func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return isMotionEnabled ? MotionTransition() : nil
}
}
extension MotionTransition: UIViewControllerAnimatedTransitioning {
@objc(animateTransition:)
open func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
prepareToViewController()
prepareFromViewController()
fromViewController.motionDelegate?.motion?(transition: self, willTransition: fromView, toView: toView)
Motion.delay(modifiedDelay) { [weak self] in
guard let s = self else {
return
}
s.prepareContainerView()
s.prepareTransitionSnapshot()
s.prepareTransitionPairs()
s.prepareTransitionView()
s.prepareTransitionBackgroundView()
s.prepareTransitionToView()
s.prepareTransitionAnimation()
}
}
@objc(transitionDuration:)
open func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return delay + duration
}
}
extension MotionTransition {
fileprivate func prepareToViewController() {
guard let v = transitionContext.viewController(forKey: .to) else {
return
}
toViewController = v
}
fileprivate func prepareFromViewController() {
guard let v = transitionContext.viewController(forKey: .from) else {
return
}
fromViewController = v
}
fileprivate func prepareContainerView() {
containerView = transitionContext.containerView
}
fileprivate func prepareTransitionSnapshot() {
transitionSnapshot = fromView.motionSnapshot(afterUpdates: true, shouldHide: false)
transitionSnapshot.frame = containerView.bounds
containerView.insertSubview(transitionSnapshot, aboveSubview: fromView)
}
fileprivate func prepareTransitionPairs() {
for from in fromSubviews {
for to in toSubviews {
guard to.motionIdentifier == from.motionIdentifier else {
continue
}
transitionPairs.append((from, to))
}
}
}
fileprivate func prepareTransitionView() {
transitionView.frame = containerView.bounds
containerView.insertSubview(transitionView, belowSubview: transitionSnapshot)
}
fileprivate func prepareTransitionBackgroundView() {
transitionBackgroundView.backgroundColor = isPresenting ? .clear : fromView.backgroundColor ?? .clear
transitionBackgroundView.frame = transitionView.bounds
transitionView.addSubview(transitionBackgroundView)
}
fileprivate func prepareTransitionToView() {
toView.isHidden = isPresenting
containerView.insertSubview(toView, belowSubview: transitionView)
toView.updateConstraints()
toView.setNeedsLayout()
toView.layoutIfNeeded()
}
fileprivate func prepareTransitionAnimation() {
addTransitionAnimations()
addBackgroundMotionAnimation()
cleanupAnimation()
removeTransitionSnapshot()
}
}
extension MotionTransition {
fileprivate func addTransitionAnimations() {
for (from, to) in transitionPairs {
var snapshotAnimations = [CABasicAnimation]()
var snapshotChildAnimations = [CABasicAnimation]()
let sizeAnimation = Motion.size(to.bounds.size)
let cornerRadiusAnimation = Motion.corner(radius: to.cornerRadius)
snapshotAnimations.append(sizeAnimation)
snapshotAnimations.append(cornerRadiusAnimation)
snapshotAnimations.append(Motion.position(to: to.motionPosition))
snapshotAnimations.append(Motion.transform(transform: to.motionTransform))
snapshotAnimations.append(Motion.background(color: to.backgroundColor ?? .clear))
snapshotChildAnimations.append(cornerRadiusAnimation)
snapshotChildAnimations.append(sizeAnimation)
snapshotChildAnimations.append(Motion.position(x: to.bounds.width / 2, y: to.bounds.height / 2))
let d = motionDuration(animations: to.motionAnimations)
let snapshot = from.motionSnapshot(afterUpdates: true)
transitionView.addSubview(snapshot)
Motion.delay(motionDelay(animations: to.motionAnimations)) { [weak self, weak to] in
guard let s = self else {
return
}
guard let v = to else {
return
}
let tf = s.motionTimingFunction(animations: v.motionAnimations)
let snapshotGroup = Motion.animate(group: snapshotAnimations, duration: d)
snapshotGroup.fillMode = MotionAnimationFillModeToValue(mode: .forwards)
snapshotGroup.isRemovedOnCompletion = false
snapshotGroup.timingFunction = MotionAnimationTimingFunctionToValue(timingFunction: tf)
let snapshotChildGroup = Motion.animate(group: snapshotChildAnimations, duration: d)
snapshotChildGroup.fillMode = MotionAnimationFillModeToValue(mode: .forwards)
snapshotChildGroup.isRemovedOnCompletion = false
snapshotChildGroup.timingFunction = MotionAnimationTimingFunctionToValue(timingFunction: tf)
snapshot.animate(snapshotGroup)
snapshot.subviews.first!.animate(snapshotChildGroup)
}
}
}
fileprivate func addBackgroundMotionAnimation() {
transitionBackgroundView.motion(.backgroundColor(isPresenting ? toView.backgroundColor ?? .clear : .clear), .duration(transitionDuration(using: transitionContext)))
}
}
extension MotionTransition {
fileprivate func motionDelay(animations: [MotionAnimation]) -> TimeInterval {
var t: TimeInterval = 0
for a in animations {
switch a {
case let .delay(time):
if time > delay {
delay = time
}
t = time
default:break
}
}
return t
}
fileprivate func motionDuration(animations: [MotionAnimation]) -> TimeInterval {
var t: TimeInterval = 0.35
for a in animations {
switch a {
case let .duration(time):
if time > duration {
duration = time
}
t = time
default:break
}
}
return t
}
fileprivate func motionTimingFunction(animations: [MotionAnimation]) -> MotionAnimationTimingFunction {
var t = MotionAnimationTimingFunction.easeInEaseOut
for a in animations {
switch a {
case let .timingFunction(timingFunction):
t = timingFunction
default:break
}
}
return t
}
}
extension MotionTransition {
fileprivate func cleanupAnimation() {
Motion.delay(transitionDuration(using: transitionContext) + modifiedDelay) { [weak self] in
guard let s = self else {
return
}
s.showToSubviews()
s.clearTransitionView()
s.clearTransitionPairs()
s.completeTransition()
}
}
fileprivate func removeTransitionSnapshot() {
Motion.delay(delay) { [weak self] in
self?.transitionSnapshot.removeFromSuperview()
}
}
fileprivate func showToSubviews() {
toSubviews.forEach {
$0.isHidden = false
}
toView.isHidden = false
}
fileprivate func clearTransitionPairs() {
transitionPairs.removeAll()
}
fileprivate func clearTransitionView() {
transitionView.removeFromSuperview()
transitionView.subviews.forEach {
$0.removeFromSuperview()
}
}
fileprivate func completeTransition() {
toViewController.motionDelegate?.motion?(transition: self, didTransition: fromView, toView: toView)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
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