Commit f1dcf964 by Daniel Dahan

updated Motion+CAMediaTimingFunction class extension

parent cab3d97c
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
961409B81E43D21300E7BA99 /* Motion.h in Headers */ = {isa = PBXBuildFile; fileRef = 96C98DED1E438A5700B22906 /* Motion.h */; settings = {ATTRIBUTES = (Public, ); }; }; 961409B81E43D21300E7BA99 /* Motion.h in Headers */ = {isa = PBXBuildFile; fileRef = 96C98DED1E438A5700B22906 /* Motion.h */; settings = {ATTRIBUTES = (Public, ); }; };
963150D21EE50DA6002B0D42 /* Motion+Obj-C.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963150D11EE50DA6002B0D42 /* Motion+Obj-C.swift */; };
96AEB68D1EE4610F009A3BE0 /* MotionAnimatorViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB6651EE4610F009A3BE0 /* MotionAnimatorViewContext.swift */; }; 96AEB68D1EE4610F009A3BE0 /* MotionAnimatorViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB6651EE4610F009A3BE0 /* MotionAnimatorViewContext.swift */; };
96AEB68E1EE4610F009A3BE0 /* MotionCoreAnimationViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB6661EE4610F009A3BE0 /* MotionCoreAnimationViewContext.swift */; }; 96AEB68E1EE4610F009A3BE0 /* MotionCoreAnimationViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB6661EE4610F009A3BE0 /* MotionCoreAnimationViewContext.swift */; };
96AEB68F1EE4610F009A3BE0 /* MotionDefaultAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB6671EE4610F009A3BE0 /* MotionDefaultAnimator.swift */; }; 96AEB68F1EE4610F009A3BE0 /* MotionDefaultAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB6671EE4610F009A3BE0 /* MotionDefaultAnimator.swift */; };
...@@ -15,7 +16,7 @@ ...@@ -15,7 +16,7 @@
96AEB6911EE4610F009A3BE0 /* MotionDebugPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB66A1EE4610F009A3BE0 /* MotionDebugPlugin.swift */; }; 96AEB6911EE4610F009A3BE0 /* MotionDebugPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB66A1EE4610F009A3BE0 /* MotionDebugPlugin.swift */; };
96AEB6921EE4610F009A3BE0 /* MotionDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB66B1EE4610F009A3BE0 /* MotionDebugView.swift */; }; 96AEB6921EE4610F009A3BE0 /* MotionDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB66B1EE4610F009A3BE0 /* MotionDebugView.swift */; };
96AEB6931EE4610F009A3BE0 /* DefaultAnimationPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB66C1EE4610F009A3BE0 /* DefaultAnimationPreprocessor.swift */; }; 96AEB6931EE4610F009A3BE0 /* DefaultAnimationPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB66C1EE4610F009A3BE0 /* DefaultAnimationPreprocessor.swift */; };
96AEB6941EE4610F009A3BE0 /* MotionTransition+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB66E1EE4610F009A3BE0 /* MotionTransition+Array.swift */; }; 96AEB6941EE4610F009A3BE0 /* Motion+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB66E1EE4610F009A3BE0 /* Motion+Array.swift */; };
96AEB6951EE4610F009A3BE0 /* Motion+CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB66F1EE4610F009A3BE0 /* Motion+CALayer.swift */; }; 96AEB6951EE4610F009A3BE0 /* Motion+CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB66F1EE4610F009A3BE0 /* Motion+CALayer.swift */; };
96AEB6961EE4610F009A3BE0 /* Motion+CAMediaTimingFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB6701EE4610F009A3BE0 /* Motion+CAMediaTimingFunction.swift */; }; 96AEB6961EE4610F009A3BE0 /* Motion+CAMediaTimingFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB6701EE4610F009A3BE0 /* Motion+CAMediaTimingFunction.swift */; };
96AEB6971EE4610F009A3BE0 /* Motion+CG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB6711EE4610F009A3BE0 /* Motion+CG.swift */; }; 96AEB6971EE4610F009A3BE0 /* Motion+CG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96AEB6711EE4610F009A3BE0 /* Motion+CG.swift */; };
...@@ -46,6 +47,7 @@ ...@@ -46,6 +47,7 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
963150D11EE50DA6002B0D42 /* Motion+Obj-C.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+Obj-C.swift"; sourceTree = "<group>"; };
96AEB6651EE4610F009A3BE0 /* MotionAnimatorViewContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimatorViewContext.swift; sourceTree = "<group>"; }; 96AEB6651EE4610F009A3BE0 /* MotionAnimatorViewContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimatorViewContext.swift; sourceTree = "<group>"; };
96AEB6661EE4610F009A3BE0 /* MotionCoreAnimationViewContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionCoreAnimationViewContext.swift; sourceTree = "<group>"; }; 96AEB6661EE4610F009A3BE0 /* MotionCoreAnimationViewContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionCoreAnimationViewContext.swift; sourceTree = "<group>"; };
96AEB6671EE4610F009A3BE0 /* MotionDefaultAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionDefaultAnimator.swift; sourceTree = "<group>"; }; 96AEB6671EE4610F009A3BE0 /* MotionDefaultAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionDefaultAnimator.swift; sourceTree = "<group>"; };
...@@ -53,7 +55,7 @@ ...@@ -53,7 +55,7 @@
96AEB66A1EE4610F009A3BE0 /* MotionDebugPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionDebugPlugin.swift; sourceTree = "<group>"; }; 96AEB66A1EE4610F009A3BE0 /* MotionDebugPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionDebugPlugin.swift; sourceTree = "<group>"; };
96AEB66B1EE4610F009A3BE0 /* MotionDebugView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionDebugView.swift; sourceTree = "<group>"; }; 96AEB66B1EE4610F009A3BE0 /* MotionDebugView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionDebugView.swift; sourceTree = "<group>"; };
96AEB66C1EE4610F009A3BE0 /* DefaultAnimationPreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultAnimationPreprocessor.swift; sourceTree = "<group>"; }; 96AEB66C1EE4610F009A3BE0 /* DefaultAnimationPreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultAnimationPreprocessor.swift; sourceTree = "<group>"; };
96AEB66E1EE4610F009A3BE0 /* MotionTransition+Array.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MotionTransition+Array.swift"; sourceTree = "<group>"; }; 96AEB66E1EE4610F009A3BE0 /* Motion+Array.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+Array.swift"; sourceTree = "<group>"; };
96AEB66F1EE4610F009A3BE0 /* Motion+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+CALayer.swift"; sourceTree = "<group>"; }; 96AEB66F1EE4610F009A3BE0 /* Motion+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+CALayer.swift"; sourceTree = "<group>"; };
96AEB6701EE4610F009A3BE0 /* Motion+CAMediaTimingFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+CAMediaTimingFunction.swift"; sourceTree = "<group>"; }; 96AEB6701EE4610F009A3BE0 /* Motion+CAMediaTimingFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+CAMediaTimingFunction.swift"; sourceTree = "<group>"; };
96AEB6711EE4610F009A3BE0 /* Motion+CG.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+CG.swift"; sourceTree = "<group>"; }; 96AEB6711EE4610F009A3BE0 /* Motion+CG.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Motion+CG.swift"; sourceTree = "<group>"; };
...@@ -121,7 +123,7 @@ ...@@ -121,7 +123,7 @@
96AEB66D1EE4610F009A3BE0 /* Extensions */ = { 96AEB66D1EE4610F009A3BE0 /* Extensions */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
96AEB66E1EE4610F009A3BE0 /* MotionTransition+Array.swift */, 96AEB66E1EE4610F009A3BE0 /* Motion+Array.swift */,
96AEB66F1EE4610F009A3BE0 /* Motion+CALayer.swift */, 96AEB66F1EE4610F009A3BE0 /* Motion+CALayer.swift */,
96AEB6701EE4610F009A3BE0 /* Motion+CAMediaTimingFunction.swift */, 96AEB6701EE4610F009A3BE0 /* Motion+CAMediaTimingFunction.swift */,
96AEB6711EE4610F009A3BE0 /* Motion+CG.swift */, 96AEB6711EE4610F009A3BE0 /* Motion+CG.swift */,
...@@ -129,6 +131,7 @@ ...@@ -129,6 +131,7 @@
96AEB6731EE4610F009A3BE0 /* Motion+UIKit.swift */, 96AEB6731EE4610F009A3BE0 /* Motion+UIKit.swift */,
96AEB6741EE4610F009A3BE0 /* Motion+UIView.swift */, 96AEB6741EE4610F009A3BE0 /* Motion+UIView.swift */,
96AEB6751EE4610F009A3BE0 /* Motion+UIViewController.swift */, 96AEB6751EE4610F009A3BE0 /* Motion+UIViewController.swift */,
963150D11EE50DA6002B0D42 /* Motion+Obj-C.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -288,7 +291,7 @@ ...@@ -288,7 +291,7 @@
96AEB6A21EE4610F009A3BE0 /* MotionTransition+MotionStringConvertible.swift in Sources */, 96AEB6A21EE4610F009A3BE0 /* MotionTransition+MotionStringConvertible.swift in Sources */,
96AEB6A51EE4610F009A3BE0 /* MotionTargetState.swift in Sources */, 96AEB6A51EE4610F009A3BE0 /* MotionTargetState.swift in Sources */,
96AEB6961EE4610F009A3BE0 /* Motion+CAMediaTimingFunction.swift in Sources */, 96AEB6961EE4610F009A3BE0 /* Motion+CAMediaTimingFunction.swift in Sources */,
96AEB6941EE4610F009A3BE0 /* MotionTransition+Array.swift in Sources */, 96AEB6941EE4610F009A3BE0 /* Motion+Array.swift in Sources */,
96AEB6951EE4610F009A3BE0 /* Motion+CALayer.swift in Sources */, 96AEB6951EE4610F009A3BE0 /* Motion+CALayer.swift in Sources */,
96AEB6981EE4610F009A3BE0 /* Motion+DispatchQueue.swift in Sources */, 96AEB6981EE4610F009A3BE0 /* Motion+DispatchQueue.swift in Sources */,
96AEB6A61EE4610F009A3BE0 /* MotionTypes.swift in Sources */, 96AEB6A61EE4610F009A3BE0 /* MotionTypes.swift in Sources */,
...@@ -308,6 +311,7 @@ ...@@ -308,6 +311,7 @@
96AEB68D1EE4610F009A3BE0 /* MotionAnimatorViewContext.swift in Sources */, 96AEB68D1EE4610F009A3BE0 /* MotionAnimatorViewContext.swift in Sources */,
96AEB6B01EE4610F009A3BE0 /* SourcePreprocessor.swift in Sources */, 96AEB6B01EE4610F009A3BE0 /* SourcePreprocessor.swift in Sources */,
96AEB6911EE4610F009A3BE0 /* MotionDebugPlugin.swift in Sources */, 96AEB6911EE4610F009A3BE0 /* MotionDebugPlugin.swift in Sources */,
963150D21EE50DA6002B0D42 /* Motion+Obj-C.swift in Sources */,
96AEB6AF1EE4610F009A3BE0 /* MatchPreprocessor.swift in Sources */, 96AEB6AF1EE4610F009A3BE0 /* MatchPreprocessor.swift in Sources */,
96AEB69E1EE4610F009A3BE0 /* MotionController.swift in Sources */, 96AEB69E1EE4610F009A3BE0 /* MotionController.swift in Sources */,
96AEB6A11EE4610F009A3BE0 /* MotionTransition.swift in Sources */, 96AEB6A11EE4610F009A3BE0 /* MotionTransition.swift in Sources */,
......
...@@ -29,43 +29,82 @@ ...@@ -29,43 +29,82 @@
import UIKit import UIKit
internal class MotionAnimatorViewContext { internal class MotionAnimatorViewContext {
var animator: MotionAnimator? /// An optional reference to a MotionAnimator.
var snapshot: UIView var animator: MotionAnimator?
var duration: TimeInterval = 0
/// A reference to the snapshot UIView.
var snapshot: UIView
/// Animation duration time.
var duration: TimeInterval = 0
/// The animation target state.
var targetState: MotionTargetState
var targetState: MotionTargetState /// 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?.context.container
}
// computed /**
var currentTime: TimeInterval { An initializer.
return snapshot.layer.convertTime(CACurrentMediaTime(), from: nil) - Parameter animator: A MotionAnimator.
} - Parameter snapshot: A UIView.
var container: UIView? { - Parameter targetState: A MotionTargetState.
return animator?.context.container */
} required init(animator: MotionAnimator, snapshot: UIView, targetState: MotionTargetState) {
self.animator = animator
self.snapshot = snapshot
self.targetState = targetState
}
class func canAnimate(view: UIView, state: MotionTargetState, appearing: Bool) -> Bool { /// Cleans the context.
return false func clean() {
} animator = nil
}
/**
A class function that determines if a view can be animated
to a given state.
- Parameter view: A UIView.
- Parameter state: A MotionTargetState.
- Parameter isAppearing: A boolean that determines whether the
view is appearing.
*/
class func canAnimate(view: UIView, state: MotionTargetState, isAppearing: Bool) -> Bool {
return false
}
func apply(state: MotionTargetState) { /**
} Applies the given state to the target state.
- Parameter state: A MotionTargetState.
*/
func apply(state: MotionTargetState) {}
/**
Resumes the animation with a given elapsed time and
optional reversed boolean.
- Parameter elapsedTime: A TimeInterval.
- Parameter isReversed: A boolean to reverse the animation
or not.
*/
func resume(elapsedTime: TimeInterval, isReversed: Bool) {}
/**
Moves the animation to the given elapsed time.
- Parameter to elapsedTime: A TimeInterval.
*/
func seek(to elapsedTime: TimeInterval) {}
func resume(timePassed: TimeInterval, reverse: Bool) { /**
} Starts the animations with an appearing boolean flag.
- Parameter isAppearing: A boolean value whether the view
func seek(timePassed: TimeInterval) { is appearing or not.
} */
func startAnimations(isAppearing: Bool) {}
func clean() {
animator = nil
}
func startAnimations(appearing: Bool) {
}
required init(animator: MotionAnimator, snapshot: UIView, targetState: MotionTargetState) {
self.animator = animator
self.snapshot = snapshot
self.targetState = targetState
}
} }
...@@ -59,16 +59,16 @@ internal class MotionDefaultAnimator<ViewContext: MotionAnimatorViewContext>: Mo ...@@ -59,16 +59,16 @@ internal class MotionDefaultAnimator<ViewContext: MotionAnimatorViewContext>: Mo
var viewContexts: [UIView: ViewContext] = [:] var viewContexts: [UIView: ViewContext] = [:]
internal var insertToViewFirst = false internal var insertToViewFirst = false
public func seekTo(timePassed: TimeInterval) { public func seekTo(elapsedTime: TimeInterval) {
for viewContext in viewContexts.values { for viewContext in viewContexts.values {
viewContext.seek(timePassed: timePassed) viewContext.seek(to: elapsedTime)
} }
} }
public func resume(timePassed: TimeInterval, reverse: Bool) -> TimeInterval { public func resume(elapsedTime: TimeInterval, isReversed: Bool) -> TimeInterval {
var duration: TimeInterval = 0 var duration: TimeInterval = 0
for (_, context) in viewContexts { for (_, context) in viewContexts {
context.resume(timePassed: timePassed, reverse: reverse) context.resume(elapsedTime: elapsedTime, isReversed: isReversed)
duration = max(duration, context.duration) duration = max(duration, context.duration)
} }
return duration return duration
...@@ -80,20 +80,20 @@ internal class MotionDefaultAnimator<ViewContext: MotionAnimatorViewContext>: Mo ...@@ -80,20 +80,20 @@ internal class MotionDefaultAnimator<ViewContext: MotionAnimatorViewContext>: Mo
} }
} }
public func canAnimate(view: UIView, appearing: Bool) -> Bool { public func canAnimate(view: UIView, isAppearing: Bool) -> Bool {
guard let state = context[view] else { return false } guard let state = context[view] else { return false }
return ViewContext.canAnimate(view: view, state: state, appearing: appearing) return ViewContext.canAnimate(view: view, state: state, isAppearing: isAppearing)
} }
public func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval { public func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval {
var duration: TimeInterval = 0 var duration: TimeInterval = 0
if insertToViewFirst { if insertToViewFirst {
for v in toViews { animate(view: v, appearing: true) } for v in toViews { animate(view: v, isAppearing: true) }
for v in fromViews { animate(view: v, appearing: false) } for v in fromViews { animate(view: v, isAppearing: false) }
} else { } else {
for v in fromViews { animate(view: v, appearing: false) } for v in fromViews { animate(view: v, isAppearing: false) }
for v in toViews { animate(view: v, appearing: true) } for v in toViews { animate(view: v, isAppearing: true) }
} }
for viewContext in viewContexts.values { for viewContext in viewContexts.values {
...@@ -103,11 +103,11 @@ internal class MotionDefaultAnimator<ViewContext: MotionAnimatorViewContext>: Mo ...@@ -103,11 +103,11 @@ internal class MotionDefaultAnimator<ViewContext: MotionAnimatorViewContext>: Mo
return duration return duration
} }
func animate(view: UIView, appearing: Bool) { func animate(view: UIView, isAppearing: Bool) {
let snapshot = context.snapshotView(for: view) let snapshot = context.snapshotView(for: view)
let viewContext = ViewContext(animator:self, snapshot: snapshot, targetState: context[view]!) let viewContext = ViewContext(animator:self, snapshot: snapshot, targetState: context[view]!)
viewContexts[view] = viewContext viewContexts[view] = viewContext
viewContext.startAnimations(appearing: appearing) viewContext.startAnimations(isAppearing: isAppearing)
} }
public func clean() { public func clean() {
......
...@@ -33,17 +33,17 @@ internal class MotionViewPropertyViewContext: MotionAnimatorViewContext { ...@@ -33,17 +33,17 @@ internal class MotionViewPropertyViewContext: MotionAnimatorViewContext {
var viewPropertyAnimator: UIViewPropertyAnimator? var viewPropertyAnimator: UIViewPropertyAnimator?
override class func canAnimate(view: UIView, state: MotionTargetState, appearing: Bool) -> Bool { override class func canAnimate(view: UIView, state: MotionTargetState, isAppearing: Bool) -> Bool {
return view is UIVisualEffectView && state.opacity != nil return view is UIVisualEffectView && state.opacity != nil
} }
override func resume(timePassed: TimeInterval, reverse: Bool) { override func resume(elapsedTime: TimeInterval, isReversed: Bool) {
viewPropertyAnimator?.finishAnimation(at: reverse ? .start : .end) viewPropertyAnimator?.finishAnimation(at: isReversed ? .start : .end)
} }
override func seek(timePassed: TimeInterval) { override func seek(to elapsedTime: TimeInterval) {
viewPropertyAnimator?.pauseAnimation() viewPropertyAnimator?.pauseAnimation()
viewPropertyAnimator?.fractionComplete = CGFloat(timePassed / duration) viewPropertyAnimator?.fractionComplete = CGFloat(elapsedTime / duration)
} }
override func clean() { override func clean() {
...@@ -52,15 +52,15 @@ internal class MotionViewPropertyViewContext: MotionAnimatorViewContext { ...@@ -52,15 +52,15 @@ internal class MotionViewPropertyViewContext: MotionAnimatorViewContext {
viewPropertyAnimator = nil viewPropertyAnimator = nil
} }
override func startAnimations(appearing: Bool) { override func startAnimations(isAppearing: Bool) {
guard let visualEffectView = snapshot as? UIVisualEffectView else { return } guard let visualEffectView = snapshot as? UIVisualEffectView else { return }
let appearedEffect = visualEffectView.effect let appearedEffect = visualEffectView.effect
let disappearedEffect = targetState.opacity == 0 ? nil : visualEffectView.effect let disappearedEffect = targetState.opacity == 0 ? nil : visualEffectView.effect
visualEffectView.effect = appearing ? disappearedEffect : appearedEffect visualEffectView.effect = isAppearing ? disappearedEffect : appearedEffect
duration = targetState.duration! duration = targetState.duration!
viewPropertyAnimator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut) { viewPropertyAnimator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut) {
visualEffectView.effect = appearing ? appearedEffect : disappearedEffect visualEffectView.effect = isAppearing ? appearedEffect : disappearedEffect
} }
viewPropertyAnimator!.startAnimation() viewPropertyAnimator!.startAnimation()
} }
......
...@@ -59,7 +59,7 @@ public class MotionDebugPlugin: MotionPlugin { ...@@ -59,7 +59,7 @@ public class MotionDebugPlugin: MotionPlugin {
return .infinity return .infinity
} }
public override func resume(timePassed: TimeInterval, reverse: Bool) -> TimeInterval { public override func resume(elapsedTime: TimeInterval, isReversed: Bool) -> TimeInterval {
guard let debugView = debugView else { return 0.4 } guard let debugView = debugView else { return 0.4 }
debugView.delegate = nil debugView.delegate = nil
......
...@@ -217,14 +217,14 @@ class DefaultAnimationPreprocessor: BasePreprocessor { ...@@ -217,14 +217,14 @@ class DefaultAnimationPreprocessor: BasePreprocessor {
self.motion = motion self.motion = motion
} }
func shift(direction: MotionDefaultAnimationType.Direction, appearing: Bool, size: CGSize? = nil, transpose: Bool = false) -> CGPoint { func shift(direction: MotionDefaultAnimationType.Direction, isAppearing: Bool, size: CGSize? = nil, transpose: Bool = false) -> CGPoint {
let size = size ?? context.container.bounds.size let size = size ?? context.container.bounds.size
let rtn: CGPoint let rtn: CGPoint
switch direction { switch direction {
case .left, .right: case .left, .right:
rtn = CGPoint(x: (direction == .right) == appearing ? -size.width : size.width, y: 0) rtn = CGPoint(x: (direction == .right) == isAppearing ? -size.width : size.width, y: 0)
case .up, .down: case .up, .down:
rtn = CGPoint(x: 0, y: (direction == .down) == appearing ? -size.height : size.height) rtn = CGPoint(x: 0, y: (direction == .down) == isAppearing ? -size.height : size.height)
} }
if transpose { if transpose {
return CGPoint(x: rtn.y, y: rtn.x) return CGPoint(x: rtn.y, y: rtn.x)
...@@ -261,7 +261,7 @@ class DefaultAnimationPreprocessor: BasePreprocessor { ...@@ -261,7 +261,7 @@ class DefaultAnimationPreprocessor: BasePreprocessor {
} }
if case .auto = defaultAnimation { if case .auto = defaultAnimation {
if animators!.contains(where: { $0.canAnimate(view: toView, appearing: true) || $0.canAnimate(view: fromView, appearing: false) }) { if animators!.contains(where: { $0.canAnimate(view: toView, isAppearing: true) || $0.canAnimate(view: fromView, isAppearing: false) }) {
defaultAnimation = .none defaultAnimation = .none
} else if inNavigationController { } else if inNavigationController {
defaultAnimation = presenting ? .push(direction:.left) : .pull(direction:.right) defaultAnimation = presenting ? .push(direction:.left) : .pull(direction:.right)
...@@ -286,28 +286,28 @@ class DefaultAnimationPreprocessor: BasePreprocessor { ...@@ -286,28 +286,28 @@ class DefaultAnimationPreprocessor: BasePreprocessor {
.masksToBounds(false)] .masksToBounds(false)]
switch defaultAnimation { switch defaultAnimation {
case .push(let direction): case .push(let direction):
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)), context[toView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true)),
.shadowOpacity(0), .shadowOpacity(0),
.beginWith(modifiers: shadowState), .beginWith(modifiers: shadowState),
.timingFunction(.deceleration)]) .timingFunction(.deceleration)])
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false) / 3), context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false) / 3),
.overlay(color: .black, opacity: 0.1), .overlay(color: .black, opacity: 0.1),
.timingFunction(.deceleration)]) .timingFunction(.deceleration)])
case .pull(let direction): case .pull(let direction):
motion.insertToViewFirst = true motion.insertToViewFirst = true
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)), context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false)),
.shadowOpacity(0), .shadowOpacity(0),
.beginWith(modifiers: shadowState)]) .beginWith(modifiers: shadowState)])
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true) / 3), context[toView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true) / 3),
.overlay(color: .black, opacity: 0.1)]) .overlay(color: .black, opacity: 0.1)])
case .slide(let direction): case .slide(let direction):
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false))]) context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false))])
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true))]) context[toView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true))])
case .zoomSlide(let direction): case .zoomSlide(let direction):
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)), .scale(0.8)]) context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false)), .scale(0.8)])
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)), .scale(0.8)]) context[toView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true)), .scale(0.8)])
case .cover(let direction): case .cover(let direction):
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)), context[toView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true)),
.shadowOpacity(0), .shadowOpacity(0),
.beginWith(modifiers: shadowState), .beginWith(modifiers: shadowState),
.timingFunction(.deceleration)]) .timingFunction(.deceleration)])
...@@ -315,12 +315,12 @@ class DefaultAnimationPreprocessor: BasePreprocessor { ...@@ -315,12 +315,12 @@ class DefaultAnimationPreprocessor: BasePreprocessor {
.timingFunction(.deceleration)]) .timingFunction(.deceleration)])
case .uncover(let direction): case .uncover(let direction):
motion.insertToViewFirst = true motion.insertToViewFirst = true
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)), context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false)),
.shadowOpacity(0), .shadowOpacity(0),
.beginWith(modifiers: shadowState)]) .beginWith(modifiers: shadowState)])
context[toView]!.append(contentsOf: [.overlay(color: .black, opacity: 0.1)]) context[toView]!.append(contentsOf: [.overlay(color: .black, opacity: 0.1)])
case .pageIn(let direction): case .pageIn(let direction):
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)), context[toView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: true)),
.shadowOpacity(0), .shadowOpacity(0),
.beginWith(modifiers: shadowState), .beginWith(modifiers: shadowState),
.timingFunction(.deceleration)]) .timingFunction(.deceleration)])
...@@ -329,7 +329,7 @@ class DefaultAnimationPreprocessor: BasePreprocessor { ...@@ -329,7 +329,7 @@ class DefaultAnimationPreprocessor: BasePreprocessor {
.timingFunction(.deceleration)]) .timingFunction(.deceleration)])
case .pageOut(let direction): case .pageOut(let direction):
motion.insertToViewFirst = true motion.insertToViewFirst = true
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)), context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, isAppearing: false)),
.shadowOpacity(0), .shadowOpacity(0),
.beginWith(modifiers: shadowState)]) .beginWith(modifiers: shadowState)])
context[toView]!.append(contentsOf: [.scale(0.7), context[toView]!.append(contentsOf: [.scale(0.7),
......
...@@ -25,45 +25,91 @@ ...@@ -25,45 +25,91 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
import UIKit import UIKit
internal enum CAMediaTimingFunctionType {
case linear
case easeIn
case easeOut
case easeInOut
case standard
case deceleration
case acceleration
case sharp
case easeOutBack
}
internal extension CAMediaTimingFunction { internal extension CAMediaTimingFunction {
// default // default
static let linear = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) static let linear = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
static let easeIn = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn) static let easeIn = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
static let easeOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) static let easeOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
static let easeInOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) static let easeInOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// material // material
static let standard = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 0.2, 1.0) static let standard = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 0.2, 1.0)
static let deceleration = CAMediaTimingFunction(controlPoints: 0.0, 0.0, 0.2, 1) static let deceleration = CAMediaTimingFunction(controlPoints: 0.0, 0.0, 0.2, 1)
static let acceleration = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 1, 1) static let acceleration = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 1, 1)
static let sharp = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 0.6, 1) static let sharp = CAMediaTimingFunction(controlPoints: 0.4, 0.0, 0.6, 1)
// easing.net // easing.net
static let easeOutBack = CAMediaTimingFunction(controlPoints: 0.175, 0.885, 0.32, 1.275) static let easeOutBack = CAMediaTimingFunction(controlPoints: 0.175, 0.885, 0.32, 1.75)
static func from(name: String) -> CAMediaTimingFunction? { /**
switch name { Converts a string name matching a CAMediaTimingFunctionType to a
case "linear": CAMediaTimingFunction value.
return .linear - Parameter mediaTimingFunctionType: A String.
case "easeIn": */
return .easeIn static func from(mediaTimingFunctionType: String) -> CAMediaTimingFunction? {
case "easeOut": switch mediaTimingFunctionType {
return .easeOut case "linear":
case "easeInOut": return .linear
return .easeInOut case "easeIn":
case "standard": return .easeIn
return .standard case "easeOut":
case "deceleration": return .easeOut
return .deceleration case "easeInOut":
case "acceleration": return .easeInOut
return .acceleration case "standard":
case "sharp": return .standard
return .sharp case "deceleration":
default: return .deceleration
return nil case "acceleration":
return .acceleration
case "sharp":
return .sharp
case "easeOutBack":
return .easeOutBack
default:
return nil
}
}
/**
Converts a CAMediaTimingFunctionType to a CAMediaTimingFunction value.
- Parameter mediaTimingFunctionType: A CAMediaTimingFunctionType.
*/
static func from(mediaTimingFunctionType: CAMediaTimingFunctionType) -> CAMediaTimingFunction {
switch mediaTimingFunctionType {
case .linear:
return .linear
case .easeIn:
return .easeIn
case .easeOut:
return .easeOut
case .easeInOut:
return .easeInOut
case .standard:
return .standard
case .deceleration:
return .deceleration
case .acceleration:
return .acceleration
case .sharp:
return .sharp
case .easeOutBack:
return .easeOutBack
}
} }
}
} }
/*
* 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.
*/
/**
Gets the Obj-C reference for the instance object within the UIView extension.
- Parameter base: Base object.
- Parameter key: Memory key pointer.
- Parameter initializer: Object initializer.
- Returns: The associated reference for the initializer object.
*/
internal func AssociatedObject<T: Any>(base: Any, key: UnsafePointer<UInt8>, initializer: () -> T) -> T {
if let v = objc_getAssociatedObject(base, key) as? T {
return v
}
let v = initializer()
objc_setAssociatedObject(base, key, v, .OBJC_ASSOCIATION_RETAIN)
return v
}
/**
Sets the Obj-C reference for the instance object within the UIView extension.
- Parameter base: Base object.
- Parameter key: Memory key pointer.
- Parameter value: The object instance to set for the associated object.
- Returns: The associated reference for the initializer object.
*/
internal func AssociateObject<T: Any>(base: Any, key: UnsafePointer<UInt8>, value: T) {
objc_setAssociatedObject(base, key, value, .OBJC_ASSOCIATION_RETAIN)
}
...@@ -47,14 +47,14 @@ public class MotionController: NSObject { ...@@ -47,14 +47,14 @@ public class MotionController: NSObject {
} }
} }
let timePassed = progress * totalDuration let elapsedTime = progress * totalDuration
if interactive { if interactive {
for animator in animators { for animator in animators {
animator.seekTo(timePassed: timePassed) animator.seekTo(elapsedTime: elapsedTime)
} }
} else { } else {
for plugin in plugins where plugin.requirePerFrameCallback { for plugin in plugins where plugin.requirePerFrameCallback {
plugin.seekTo(timePassed: timePassed) plugin.seekTo(elapsedTime: elapsedTime)
} }
} }
} }
...@@ -99,14 +99,14 @@ public class MotionController: NSObject { ...@@ -99,14 +99,14 @@ public class MotionController: NSObject {
} }
func displayUpdate(_ link: CADisplayLink) { func displayUpdate(_ link: CADisplayLink) {
if transitioning, duration > 0, let beginTime = beginTime { if transitioning, duration > 0, let beginTime = beginTime {
let timePassed = CACurrentMediaTime() - beginTime let elapsedTime = CACurrentMediaTime() - beginTime
if timePassed > duration { if elapsedTime > duration {
progress = finishing ? 1 : 0 progress = finishing ? 1 : 0
self.beginTime = nil self.beginTime = nil
complete(finished: finishing) complete(finished: finishing)
} else { } else {
var completed = timePassed / totalDuration var completed = elapsedTime / totalDuration
if !finishing { if !finishing {
completed = 1 - completed completed = 1 - completed
} }
...@@ -156,8 +156,8 @@ public extension MotionController { ...@@ -156,8 +156,8 @@ public extension MotionController {
} }
var maxTime: TimeInterval = 0 var maxTime: TimeInterval = 0
for animator in self.animators { for animator in self.animators {
maxTime = max(maxTime, animator.resume(timePassed:self.progress * self.totalDuration, maxTime = max(maxTime, animator.resume(elapsedTime:self.progress * self.totalDuration,
reverse: false)) isReversed: false))
} }
self.complete(after: maxTime, finishing: true) self.complete(after: maxTime, finishing: true)
} }
...@@ -179,8 +179,8 @@ public extension MotionController { ...@@ -179,8 +179,8 @@ public extension MotionController {
if adjustedProgress < 0 { if adjustedProgress < 0 {
adjustedProgress = -adjustedProgress adjustedProgress = -adjustedProgress
} }
maxTime = max(maxTime, animator.resume(timePassed:adjustedProgress * self.totalDuration, maxTime = max(maxTime, animator.resume(elapsedTime:adjustedProgress * self.totalDuration,
reverse: true)) isReversed: true))
} }
self.complete(after: maxTime, finishing: false) self.complete(after: maxTime, finishing: false)
} }
...@@ -286,10 +286,10 @@ internal extension MotionController { ...@@ -286,10 +286,10 @@ internal extension MotionController {
animatingViews = [([UIView], [UIView])]() animatingViews = [([UIView], [UIView])]()
for animator in animators { for animator in animators {
let currentFromViews = context.fromViews.filter { (view: UIView) -> Bool in let currentFromViews = context.fromViews.filter { (view: UIView) -> Bool in
return animator.canAnimate(view: view, appearing: false) return animator.canAnimate(view: view, isAppearing: false)
} }
let currentToViews = context.toViews.filter { (view: UIView) -> Bool in let currentToViews = context.toViews.filter { (view: UIView) -> Bool in
return animator.canAnimate(view: view, appearing: true) return animator.canAnimate(view: view, isAppearing: true)
} }
animatingViews.append((currentFromViews, currentToViews)) animatingViews.append((currentFromViews, currentToViews))
} }
...@@ -335,10 +335,10 @@ internal extension MotionController { ...@@ -335,10 +335,10 @@ internal extension MotionController {
complete(finished: finishing) complete(finished: finishing)
return return
} }
let timePassed = (finishing ? progress : 1 - progress) * totalDuration let elapsedTime = (finishing ? progress : 1 - progress) * totalDuration
self.finishing = finishing self.finishing = finishing
self.duration = after + timePassed self.duration = after + elapsedTime
self.beginTime = CACurrentMediaTime() - timePassed self.beginTime = CACurrentMediaTime() - elapsedTime
} }
func complete(finished: Bool) { func complete(finished: Bool) {
......
...@@ -71,11 +71,11 @@ open class MotionPlugin: NSObject, MotionPreprocessor, MotionAnimator { ...@@ -71,11 +71,11 @@ open class MotionPlugin: NSObject, MotionPreprocessor, MotionAnimator {
- Parameters: - Parameters:
- context: object holding all parsed and changed modifiers, - context: object holding all parsed and changed modifiers,
- view: the view to check whether or not the plugin can handle the animation - view: the view to check whether or not the plugin can handle the animation
- appearing: true if the view is appearing(i.e. a view in destination ViewController) - isAppearing: true if the view is isAppearing(i.e. a view in destination ViewController)
If return true, Motion won't animate and won't let any other plugins animate this view. If return true, Motion won't animate and won't let any other plugins animate this view.
The view will also be hidden automatically during the animation. The view will also be hidden automatically during the animation.
*/ */
open func canAnimate(view: UIView, appearing: Bool) -> Bool { return false } open func canAnimate(view: UIView, isAppearing: Bool) -> Bool { return false }
/** /**
Perform the animation. Perform the animation.
...@@ -103,9 +103,9 @@ open class MotionPlugin: NSObject, MotionPreprocessor, MotionAnimator { ...@@ -103,9 +103,9 @@ open class MotionPlugin: NSObject, MotionPreprocessor, MotionAnimator {
This method is called when an interactive animation is in place This method is called when an interactive animation is in place
The plugin should pause the animation, and seek to the given progress The plugin should pause the animation, and seek to the given progress
- Parameters: - Parameters:
- timePassed: time of the animation to seek to. - elapsedTime: time of the animation to seek to.
*/ */
open func seekTo(timePassed: TimeInterval) {} open func seekTo(elapsedTime: TimeInterval) {}
/** /**
For supporting interactive animation only. For supporting interactive animation only.
...@@ -113,10 +113,10 @@ open class MotionPlugin: NSObject, MotionPreprocessor, MotionAnimator { ...@@ -113,10 +113,10 @@ open class MotionPlugin: NSObject, MotionPreprocessor, MotionAnimator {
This method is called when an interactive animation is ended This method is called when an interactive animation is ended
The plugin should resume the animation. The plugin should resume the animation.
- Parameters: - Parameters:
- timePassed: will be the same value since last `seekTo` - elapsedTime: will be the same value since last `seekTo`
- reverse: a boolean value indicating whether or not the animation should reverse - reverse: a boolean value indicating whether or not the animation should reverse
*/ */
open func resume(timePassed: TimeInterval, reverse: Bool) -> TimeInterval { return 0 } open func resume(elapsedTime: TimeInterval, isReversed: Bool) -> TimeInterval { return 0 }
/** /**
For supporting interactive animation only. For supporting interactive animation only.
......
...@@ -89,7 +89,7 @@ extension MotionTransition: MotionStringConvertible { ...@@ -89,7 +89,7 @@ extension MotionTransition: MotionStringConvertible {
let c3 = parameters.getFloat(2), let c3 = parameters.getFloat(2),
let c4 = parameters.getFloat(3) { let c4 = parameters.getFloat(3) {
return .timingFunction(CAMediaTimingFunction(controlPoints: c1, c2, c3, c4)) return .timingFunction(CAMediaTimingFunction(controlPoints: c1, c2, c3, c4))
} else if let name = parameters.get(0)?.name, let timingFunction = CAMediaTimingFunction.from(name:name) { } else if let name = parameters.get(0)?.name, let timingFunction = CAMediaTimingFunction.from(mediaTimingFunctionType: name) {
return .timingFunction(timingFunction) return .timingFunction(timingFunction)
} }
case "arc": case "arc":
......
...@@ -35,12 +35,12 @@ public protocol MotionPreprocessor: class { ...@@ -35,12 +35,12 @@ public protocol MotionPreprocessor: class {
public protocol MotionAnimator: class { public protocol MotionAnimator: class {
weak var context: MotionContext! { get set } weak var context: MotionContext! { get set }
func canAnimate(view: UIView, appearing: Bool) -> Bool func canAnimate(view: UIView, isAppearing: Bool) -> Bool
func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval
func clean() func clean()
func seekTo(timePassed: TimeInterval) func seekTo(elapsedTime: TimeInterval)
func resume(timePassed: TimeInterval, reverse: Bool) -> TimeInterval func resume(elapsedTime: TimeInterval, isReversed: Bool) -> TimeInterval
func apply(state: MotionTargetState, to view: UIView) func apply(state: MotionTargetState, to view: UIView)
} }
......
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