Commit 4a8fa6a7 by Daniel Dahan Committed by GitHub

Merge pull request #53 from OrkhanAlikhanov/bug-fixes

Bug fixes and MotionViewTransition
parents 6d023466 64b831e4
......@@ -87,6 +87,8 @@
96E409AB1F24F7570015A2B5 /* MotionPreprocessor.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409621F24F7370015A2B5 /* MotionPreprocessor.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E409AC1F24F7570015A2B5 /* SourcePreprocessor.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409631F24F7370015A2B5 /* SourcePreprocessor.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E409AD1F24F7570015A2B5 /* TransitionPreprocessor.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409641F24F7370015A2B5 /* TransitionPreprocessor.swift */; settings = {ATTRIBUTES = (Public, ); }; };
9D72470D21557F7000C04B48 /* MotionViewTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D72470C21557F7000C04B48 /* MotionViewTransition.swift */; };
9D7247132158A15E00C04B48 /* MotionViewTransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7247122158A15E00C04B48 /* MotionViewTransitionAnimator.swift */; };
D4A2ECE521467454003162B4 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2ECE421467454003162B4 /* Application.swift */; };
/* End PBXBuildFile section */
......@@ -137,6 +139,8 @@
96E409641F24F7370015A2B5 /* TransitionPreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionPreprocessor.swift; sourceTree = "<group>"; };
96E409BB1F24FC210015A2B5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
96E409BC1F24FC300015A2B5 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
9D72470C21557F7000C04B48 /* MotionViewTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionViewTransition.swift; sourceTree = "<group>"; };
9D7247122158A15E00C04B48 /* MotionViewTransitionAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionViewTransitionAnimator.swift; sourceTree = "<group>"; };
D4A2ECE421467454003162B4 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
......@@ -164,7 +168,9 @@
96E4093B1F24F7370015A2B5 /* Sources */,
96C98DD21E424AB000B22906 /* Products */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
};
96C98DD21E424AB000B22906 /* Products */ = {
isa = PBXGroup;
......@@ -192,6 +198,7 @@
96E4095A1F24F7370015A2B5 /* MotionModifier.swift */,
96E4095C1F24F7370015A2B5 /* MotionTargetState.swift */,
96E4095B1F24F7370015A2B5 /* MotionTransitionObserver.swift */,
9D72470C21557F7000C04B48 /* MotionViewTransition.swift */,
96E4093C1F24F7370015A2B5 /* Animator */,
96E409431F24F7370015A2B5 /* Extensions */,
96E4095D1F24F7370015A2B5 /* Preprocessors */,
......@@ -208,6 +215,7 @@
96E4093E1F24F7370015A2B5 /* MotionAnimatorViewContext.swift */,
96E4093F1F24F7370015A2B5 /* MotionCoreAnimationViewContext.swift */,
96E409421F24F7370015A2B5 /* MotionViewPropertyViewContext.swift */,
9D7247122158A15E00C04B48 /* MotionViewTransitionAnimator.swift */,
);
path = Animator;
sourceTree = "<group>";
......@@ -358,6 +366,7 @@
965FE9931FE43DE10098BDD0 /* MotionTransition+UINavigationControllerDelegate.swift in Sources */,
96E409861F24F7370015A2B5 /* MotionPreprocessor.swift in Sources */,
96E409821F24F7370015A2B5 /* CascadePreprocessor.swift in Sources */,
9D7247132158A15E00C04B48 /* MotionViewTransitionAnimator.swift in Sources */,
96E4096C1F24F7370015A2B5 /* Motion+CALayer.swift in Sources */,
96E409781F24F7370015A2B5 /* MotionCAAnimation.swift in Sources */,
96E409811F24F7370015A2B5 /* MotionTargetState.swift in Sources */,
......@@ -376,6 +385,7 @@
965FE9691FDDA1F20098BDD0 /* MotionViewOrderStrategy.swift in Sources */,
965FE97A1FE1D83D0098BDD0 /* MotionTransition.swift in Sources */,
965FE98D1FE334E10098BDD0 /* MotionTransition+UIViewControllerTransitioningDelegate.swift in Sources */,
9D72470D21557F7000C04B48 /* MotionViewTransition.swift in Sources */,
96E409871F24F7370015A2B5 /* SourcePreprocessor.swift in Sources */,
965FE9A11FE43EF80098BDD0 /* MotionTransition+CustomTransition.swift in Sources */,
96E409701F24F7370015A2B5 /* Motion+UIKit.swift in Sources */,
......
......@@ -44,11 +44,6 @@ internal class MotionAnimatorViewContext {
/// Animation duration time.
var duration: TimeInterval = 0
/// The computed current time of the snapshot layer.
var currentTime: TimeInterval {
return snapshot.layer.convertTime(CACurrentMediaTime(), from: nil)
}
/// A container view for the transition.
var container: UIView? {
return animator?.motion.context.container
......
......@@ -35,6 +35,11 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
/// A reference to the animation timing function.
fileprivate var timingFunction = CAMediaTimingFunction.standard
/// The computed current time of the snapshot layer.
var currentTime: TimeInterval {
return snapshot.layer.convertTime((animator as! MotionCoreAnimator<MotionCoreAnimationViewContext>).currentTime, from: nil)
}
/// Current animations.
var animations = [(CALayer, String, CAAnimation)]()
......
......@@ -29,6 +29,29 @@
import UIKit
internal class MotionCoreAnimator<T: MotionAnimatorViewContext>: MotionAnimator {
/**
Backing field for storing CACurrentMediaTime to ensure all
animations begin at the exact same time.
Should be invalidated using invalidateCurrentTime method
after firing all animations.
*/
private var storedCurrentTime: TimeInterval?
/// Current time for the animator.
var currentTime: TimeInterval {
if nil == storedCurrentTime {
storedCurrentTime = CACurrentMediaTime()
}
return storedCurrentTime!
}
/// Invalidates stored current time.
func invalidateCurrentTime() {
storedCurrentTime = nil
}
weak public var motion: MotionTransition!
/// A reference to the MotionContext.
......@@ -46,6 +69,7 @@ internal class MotionCoreAnimator<T: MotionAnimatorViewContext>: MotionAnimator
}
viewToContexts.removeAll()
invalidateCurrentTime()
}
/**
......@@ -55,7 +79,7 @@ internal class MotionCoreAnimator<T: MotionAnimatorViewContext>: MotionAnimator
view is appearing.
*/
func canAnimate(view: UIView, isAppearing: Bool) -> Bool {
guard let state = context[view] else {
guard let state = targetState(for: view) else {
return false
}
......@@ -103,6 +127,8 @@ internal class MotionCoreAnimator<T: MotionAnimatorViewContext>: MotionAnimator
d = max(d, v.startAnimations())
}
invalidateCurrentTime()
return d
}
......@@ -114,6 +140,8 @@ internal class MotionCoreAnimator<T: MotionAnimatorViewContext>: MotionAnimator
for v in viewToContexts.values {
v.seek(to: progress)
}
invalidateCurrentTime()
}
/**
......@@ -127,13 +155,10 @@ internal class MotionCoreAnimator<T: MotionAnimatorViewContext>: MotionAnimator
var duration: TimeInterval = 0
for (_, v) in viewToContexts {
if nil == v.targetState.duration {
v.duration = max(v.duration, v.snapshot.optimizedDuration(targetState: v.targetState) + progress)
}
duration = max(duration, v.resume(at: progress, isReversed: isReversed))
}
invalidateCurrentTime()
return duration
}
......@@ -148,6 +173,26 @@ internal class MotionCoreAnimator<T: MotionAnimatorViewContext>: MotionAnimator
}
v.apply(state: state)
invalidateCurrentTime()
}
/**
Returns MotionTargetState for the given view.
- Parameter for view: A UIView.
- Returns: A MotionTargetState.
*/
func targetState(for view: UIView) -> MotionTargetState? {
return context[view]
}
/**
Returns snapshot view for the given view.
- Parameter for view: A UIView.
- Returns: A snapshot UIView.
*/
func snapshotView(for view: UIView) -> UIView {
return context.snapshotView(for: view)
}
}
......@@ -159,7 +204,7 @@ fileprivate extension MotionCoreAnimator {
view is appearing.
*/
func createViewContext(view: UIView, isAppearing: Bool) {
viewToContexts[view] = T(animator: self, snapshot: context.snapshotView(for: view), targetState: context[view]!, isAppearing: isAppearing)
viewToContexts[view] = T(animator: self, snapshot: snapshotView(for: view), targetState: targetState(for: view)!, isAppearing: isAppearing)
}
}
......
/*
* The MIT License (MIT)
*
* Copyright (C) 2018, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* 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
internal class MotionViewTransitionAnimator: MotionCoreAnimator<MotionCoreAnimationViewContext> {
/**
Returns MotionTargetState for the given view.
- Parameter for view: A UIView.
- Returns: A MotionTargetState.
*/
override func targetState(for view: UIView) -> MotionTargetState? {
guard let modifiers = view.motionModifiers else {
return nil
}
return MotionTargetState(modifiers: modifiers)
}
/**
Returns snapshot view for the given view.
- Parameter for view: A UIView.
- Returns: A snapshot UIView.
*/
override func snapshotView(for view: UIView) -> UIView {
return view
}
}
......@@ -25,9 +25,6 @@
import UIKit
@available(iOS 10, *)
extension CALayer: CAAnimationDelegate {}
internal extension CALayer {
/// Swizzle the `add(_:forKey:) selector.
internal static var motionAddedAnimations: [(CALayer, String, CAAnimation)]? = {
......@@ -110,14 +107,11 @@ public extension CALayer {
*/
func animate(_ animations: [CAAnimation]) {
for animation in animations {
if nil == animation.delegate {
animation.delegate = self
}
if let a = animation as? CABasicAnimation {
a.fromValue = (presentation() ?? self).value(forKeyPath: a.keyPath!)
}
updateModel(animation)
if let a = animation as? CAPropertyAnimation {
add(a, forKey: a.keyPath!)
} else if let a = animation as? CAAnimationGroup {
......@@ -129,46 +123,6 @@ public extension CALayer {
}
/**
Executed when an animation has started.
- Parameter _ anim: A CAAnimation.
*/
func animationDidStart(_ anim: CAAnimation) {}
/**
A delegation function that is executed when the backing layer stops
running an animation.
- Parameter animation: The CAAnimation instance that stopped running.
- Parameter flag: A boolean that indicates if the animation stopped
because it was completed or interrupted. True if completed, false
if interrupted.
*/
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
guard let a = anim as? CAPropertyAnimation else {
if let a = (anim as? CAAnimationGroup)?.animations {
for x in a {
animationDidStop(x, finished: true)
}
}
return
}
guard let b = a as? CABasicAnimation else {
return
}
guard let v = b.toValue else {
return
}
guard let k = b.keyPath else {
return
}
setValue(v, forKeyPath: k)
removeAnimation(forKey: k)
}
/**
A function that accepts a list of MotionAnimation values and executes them.
- Parameter animations: A list of MotionAnimation values.
*/
......@@ -321,11 +275,7 @@ fileprivate extension CALayer {
}
}
let g = Motion.animate(group: anims, duration: duration)
g.fillMode = .both
g.isRemovedOnCompletion = false
g.timingFunction = ts.timingFunction
let g = Motion.animate(group: anims, timingFunction: ts.timingFunction, duration: duration)
self.animate(g)
if let v = ts.completion {
......@@ -338,3 +288,19 @@ fileprivate extension CALayer {
}
}
}
private extension CALayer {
/**
Updates the model with values provided in animation.
- Parameter animation: A CAAnimation.
*/
func updateModel(_ animation: CAAnimation) {
if let a = animation as? CABasicAnimation {
setValue(a.toValue, forKeyPath: a.keyPath!)
} else if let a = animation as? CAAnimationGroup {
a.animations?.forEach {
updateModel($0)
}
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2018, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* 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
private var MotionViewTransitionKey: UInt8 = 0
public extension UIView {
/// The MotionViewTransition instance associated with the view.
var motionViewTransition: MotionViewTransition {
get {
return AssociatedObject.get(base: self, key: &MotionViewTransitionKey) {
MotionViewTransition(self)
}
}
}
}
open class MotionViewTransition {
/// A MotionViewTransitionAnimator used during a transition.
private let animator = MotionViewTransitionAnimator()
/// A UIView whose subviews and itself will be animating during a transition.
private weak var container: UIView?
/// Maximum duration of the animations.
open private(set) var totalDuration: TimeInterval = 0
/// Progress of the current transition.
open private(set) var progress: CGFloat = 0
/// A Boolean to control if the models should be updated when animations end.
open var shouldUpdateModels: Bool = true
/// Current duration of the animations based on totalDuration and progress.
private var currentDuration: TimeInterval {
return totalDuration * Double(progress)
}
/**
An initializer that accepts a container transition view.
- Parameter container: A UIView.
*/
fileprivate init(_ container: UIView) {
self.container = container
}
/// Prepares the transition animations.
open func start() {
guard let v = container else {
return
}
totalDuration = animator.animate(fromViews: v.flattenedViewHierarchy, toViews: [])
update(0)
}
/**
Updates the elapsed time for the transition.
- Parameter progress: the current progress.
*/
open func update(_ progress: CGFloat) {
self.progress = progress.clamp(0, 1)
animator.seek(to: currentDuration)
}
/**
Cancels the interactive transition by animatin from the current state
to the **beginning** state.
- Parameter isAnimated: A boolean indicating if the completion is animated.
*/
open func cancel(isAnimated: Bool = true) {
end(isAnimated: isAnimated, isReversed: true)
}
/**
Finishes the interactive transition by animating from the current state
to the **end** state.
- Parameter isAnimated: A Boolean indicating if the completion is animated.
*/
open func finish(isAnimated: Bool = true) {
end(isAnimated: isAnimated, isReversed: false)
}
deinit {
clean()
}
}
private extension MotionViewTransition {
/**
Ends the interactive transition by animating from the current state
to the **beginning** or **end** state based on the value of isReversed.
- Parameter isAnimated: A Boolean indicating if the completion is animated.
- Parameter isReversed: A Boolean indicating the direction of completion.
*/
func end(isAnimated: Bool, isReversed: Bool) {
let duration = isAnimated ? currentDuration : isReversed ? 0 : totalDuration
/// 0.00001 is to make sure that animator adds the animations on resume.
let after = animator.resume(at: abs(duration - 0.00001), isReversed: isReversed)
updateModels()
if isAnimated {
Motion.delay(after, execute: complete)
} else {
complete()
}
}
/// Finalizes the transition.
func complete() {
clean()
removeAnimations()
}
/// Resets the transition.
func clean() {
animator.clean()
totalDuration = 0
progress = 0
}
}
private extension MotionViewTransition {
/// Updates the layers with final values of animations.
func updateModels() {
guard shouldUpdateModels else {
return
}
walkingThroughAnimations { layer, _, anim in
/// bounds.size somehow is directly set on the layer.
let toValue = anim.keyPath == "bounds.size" ? layer.bounds.size : anim.toValue
layer.setValue(toValue, forKeyPath: anim.keyPath!)
}
}
/// Removes added animations from layers.
func removeAnimations() {
guard shouldUpdateModels else {
return
}
walkingThroughAnimations { layer, key, _ in
layer.removeAnimation(forKey: key)
}
}
/**
Walks through each layer's animation and executes give closure with
CALayer, animation key String, and CABasicAnimation.
- Parameter execute: A closure accepting CALayer, String, and CABasicAnimation
which is called for each layer's animation.
*/
func walkingThroughAnimations(execute: (CALayer, String, CABasicAnimation) -> Void) {
animator.viewToContexts.keys.forEach { v in
v.layer.animations.forEach { key, animation in
if let anim = animation as? CABasicAnimation {
execute(v.layer, key, anim)
}
}
}
}
}
......@@ -65,6 +65,8 @@ extension MotionTransition {
progress = 0
totalDuration = 0
state = .possible
defaultAnimation = .auto
containerBackgroundColor = .black
isModalTransition = false
}
......@@ -139,7 +141,6 @@ extension MotionTransition {
processEndTransitionDelegation(transitionContext: tContext, fromViewController: fvc, toViewController: tvc)
} else {
processCancelTransitionDelegation(transitionContext: tContext, fromViewController: fvc, toViewController: tvc)
tContext?.cancelInteractiveTransition()
}
tContext?.completeTransition(isFinishing)
......
......@@ -28,7 +28,7 @@
import UIKit
extension MotionTransition {
public extension MotionTransition {
/**
Updates the elapsed time for the interactive transition.
- Parameter progress t: the current progress, must be between -1...1.
......
......@@ -207,8 +207,6 @@ extension Motion {
*/
public class func animate(group animations: [CAAnimation], timingFunction: CAMediaTimingFunction = .easeInOut, duration: CFTimeInterval = 0.5) -> CAAnimationGroup {
let group = CAAnimationGroup()
group.fillMode = .both
group.isRemovedOnCompletion = false
group.animations = animations
group.duration = duration
group.timingFunction = timingFunction
......@@ -675,7 +673,9 @@ internal extension MotionTransition {
return
}
tvc.beginAppearanceTransition(false, animated: true)
tvc.endAppearanceTransition()
fvc.beginAppearanceTransition(true, animated: true)
fvc.endAppearanceTransition()
processForMotionDelegate(viewController: fvc) { [weak self] in
......@@ -696,7 +696,7 @@ internal extension MotionTransition {
$0.motionDidCancelTransition?(motion: self)
}
transitionContext?.finishInteractiveTransition()
transitionContext?.cancelInteractiveTransition()
}
}
......
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