diff --git a/Sources/AnimationPlanner/AnimationPlanner.swift b/Sources/AnimationPlanner/AnimationPlanner.swift index c3e90ff..14b1686 100644 --- a/Sources/AnimationPlanner/AnimationPlanner.swift +++ b/Sources/AnimationPlanner/AnimationPlanner.swift @@ -1,272 +1,60 @@ import UIKit -/// (Deprecated, uses intermediate classes instead of ``AnimationBuilder``) -/// This class is used to add steps to your animation sequence. When starting a sequence animation with `UIView.animateSteps(_:completion:)`, a sequence object is made available through the `addSteps` closure, From within this closure each step should be added to the sequence object. +/// Chain multiple `UIView` animations with a clear declarative syntax, describing each step along the way. +/// Start by typing `AnimationPlanner.plan` and provide all of your animations from the `animations` closure. /// -/// Each method on ``AnimationSequence`` returns a reference to `Self`, enabling the use of chainging each method call. +/// Begin planning your animation by using either of the following static methods: +/// - ``plan(animations:)`` start a sequence animation where all animations are performed in order. +/// - ``group(animations:)`` start a group animation where all animations are performed simultaneously. /// -/// Setting up your animation should be done with the following methods: -/// - ``delay(_:)`` adds a delay to the sequence. Delays are cumulative and are applied to the first actual animation to be performend. -/// - ``add(duration:options:timingFunction:animations:)`` adds an animation step to the sequence, providing a specific duration and optionally the `UIView.AnimationOptions` options and a `CAMediaTimingFunction` timing function. -/// - ``addSpring(duration:delay:damping:initialVelocity:options:animations:)`` adds a spring-based animation step with the expected damping and velocity values. -/// - ``extra(_:)`` adds an ‘extra’ step to prepare state before or between steps for the next animation or perform side-effects like triggering haptic feedback. -/// - ``addGroup(with:)`` creates a ``Group`` object to which multiple animations can be added that should be performed simultaneously. -/// -/// - Note: Each animation is created right before it needs to be executed, so referencing values changed in previous steps is possible. -public class AnimationSequence { - - /// All steps currently added to the sequence - public internal(set) var steps: [SequenceAnimatable] = [] -} - -/// Extension methods that start an animation sequence, added to `UIView` by default -public protocol StepAnimatable { - - /// Start a sequence where you add each step in the `addSteps` closure. Use the provided `Sequence` object - /// to add each step which should either be an actual animation or a delay. - /// The `completion` closure is executed when the last animation has finished. - /// - Parameters: - /// - addSteps: Closure used to add steps to the provided `Sequence` object - /// - completion: Executed when the last animation has finished. - @available(*, deprecated, message: "Use `AnimationPlanner.plan` instead") - static func animateSteps(_ addSteps: (AnimationSequence) -> Void, completion: ((Bool) -> Void)?) - - /// Start a group animation where you add each animation is performed concurrently. Use the provided `Group` object - /// to add each animation. - /// The `completion` closure is executed when the last animation has finished. - /// - Parameters: - /// - addAnimations: Closure used to add animations to the provided `Group` object - /// - completion: Executed when the longest animation has finished. - @available(*, deprecated, message: "Use `AnimationPlanner.group` instead") - static func animateGroup(_ addAnimations: (AnimationSequence.SequenceGroup) -> Void, completion: ((Bool) -> Void)?) -} - -extension AnimationSequence { +/// - Tip: To get started, read and get up to speed on how to use AnimationPlanner, +/// or go through the whole documentation on ``AnimationPlanner`` to get an overview of all the available functionalities. +public struct AnimationPlanner { - /// Adds an animation to the sequence with all the expected animation parameters, adding the ability to use a `CAMediaTimingFunction` timing function for the interpolation. + /// Start a new animation sequence where animations added will be performed in order, meaning a subsequent animation starts right after the previous finishes. /// - /// Adding each steps can by done in a chain, as this method returns `Self` - /// - Note: Adding a timing function will wrap the animation in a `CATransaction` commit + /// ```swift + /// AnimationPlanner.plan { + /// Animate(duration: 0.25) { view.backgroundColor = .systemRed } + /// Wait(0.5) + /// Animate(duration: 0.5) { + /// view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) + /// }.spring(damping: 0.68) + /// } + /// ``` /// - Parameters: - /// - duration: How long the animation should last - /// - options: Options to use for the animation - /// - timingFunction: `CAMediaTimingFunction` to use for animation - /// - animations: Closure in which values to animate should be changed - /// - Returns: Returns `Self`, enabling the use of chaining mulitple calls - @discardableResult public func add( - duration: TimeInterval, - options: UIView.AnimationOptions = [], - timingFunction: CAMediaTimingFunction? = nil, - animations: @escaping () -> Void - ) -> Self { - steps.append( - Animate(duration: duration, timingFunction: timingFunction, changes: animations) - .options(options) - ) - return self + /// - animations: Add each animation using this closure. Animation added to a sequence should conform to ``GroupAnimatable``. + /// - Returns: Instance of ``RunningSequence`` to keep track of and stop animations + @discardableResult + public static func plan( + @AnimationBuilder animations builder: () -> [SequenceAnimatable] + ) -> RunningSequence { + RunningSequence(animations: builder()) + .animate() } - /// Adds a spring-based animation to the animation sequence with all the expected animation parameters. + /// Start a new group animation where animations added will be performed simultaneously, meaning all animations run at the same time. /// - /// Adding each step in the sequence can by done in a chain, as this method returns `Self` - /// - Note: Timing curves aren‘t available in this method by design, the spring itself should do all the interpolating. - /// - Parameters: - /// - duration: Amount of time (in seconds) the animation should last - /// - delay: Amount of time (in seconds) the animation should wait to start - /// - dampingRatio: Ratio for damping of spring animation (between 0 and 1) - /// - velocity: Initial velocity of spring animation (1 being full 'distance' in one second) - /// - options: Options to use for the animation - /// - animations: Closure in which values to animate should be changed - /// - Returns: Returns `Self`, enabling the use of chaining mulitple calls - @discardableResult public func addSpring( - duration: TimeInterval, - delay: TimeInterval = 0, - damping dampingRatio: CGFloat, - initialVelocity velocity: CGFloat = 0, - options: UIView.AnimationOptions = [], - animations: @escaping () -> Void - ) -> Self { - let spring = AnimateSpring(duration: duration, dampingRatio: dampingRatio, initialVelocity: velocity, changes: animations) - .options(options) - - if delay > 0 { - fatalError("Delay here is not supported") - } else { - steps.append(spring) - } - return self - } - - /// Adds a delay to the animation sequence + /// ```swift + /// AnimationPlanner.group { + /// Animate(duration: 0.5) { + /// view.frame.origin.y = 0 + /// }.delayed(0.15) + /// Animate(duration: 0.3) { + /// view.backgroundColor = .systemBlue + /// }.delayed(0.2) + /// } + /// ``` /// - /// While this adds an actual step to the sequence, in practice the next step that actually does - /// the animation will use this delay (or all previous delays leading up to that step). - /// - Parameter delay: Duration of the delay - /// - Returns: Returns `Self`, enabling the use of chaining mulitple calls - @discardableResult public func delay(_ duration: TimeInterval) -> Self { - steps.append( - Wait(duration) - ) - return self - } -} - -extension AnimationSequence { - /// Adds a step where preparations or side-effects can be handled. Comparable to a 0-duration animation, but without actually being - /// animated in a `UIView` animation closure. - /// - Parameter handler: Closure exectured at the specific time in the sequence - /// - Returns: Returns `Self`, enabling the use of chaining mulitple calls - @discardableResult public func extra(_ handler: @escaping () -> Void) -> Self { - steps.append( - Extra(perform: handler) - ) - return self - } -} - -extension AnimationSequence { - - /// Group of animation steps, all of which should be performed simultaneously - public class SequenceGroup { - - /// All animations currently added to the sequence - public internal(set) var animations: [GroupAnimatable] = [] - - /// Adds an animation to the animation group with all the available options. - /// - /// Adding each part in the group can by done in a chain, as this method returns `Self` - /// - Note: Adding a timing function will wrap the animation in a `CATransaction` commit - /// - Parameters: - /// - duration: Amount of time (in seconds) the animation should last - /// - delay: Amount of time (in seconds) the animation should wait to start - /// - options: Options to use for the animation - /// - timingFunction: `CAMediaTimingFunction` to use for animation - /// - animations: Closure in which values to animate should be changed - /// - Returns: Returns `Self`, enabling the use of chaining mulitple calls - @discardableResult public func animate( - duration: TimeInterval, - delay: TimeInterval = 0, - options: UIView.AnimationOptions = [], - timingFunction: CAMediaTimingFunction? = nil, - animations: @escaping () -> Void - ) -> Self { - var delayed = AnimateDelayed(delay: delay, duration: duration, changes: animations) - .options(options) - if let timingFunction = timingFunction { - delayed = delayed.timingFunction(timingFunction) - } - self.animations.append( - delayed - ) - return self - } - - /// Adds a spring-based animation to the animation group with all the available options. - /// - /// Adding each part in the group can by done in a chain, as this method returns `Self` - /// - Parameters: - /// - duration: Amount of time (in seconds) the animation should last - /// - delay: Amount of time (in seconds) the animation should wait to start - /// - dampingRatio: Ratio for damping of spring animation (between 0 and 1) - /// - velocity: Initial velocity of spring animation (1 being full 'distance' in one second) - /// - options: Options to use for the animation - /// - animations: Closure in which values to animate should be changed - /// - Returns: Returns `Self`, enabling the use of chaining mulitple calls - @discardableResult public func animateSpring( - duration: TimeInterval, - delay: TimeInterval = 0, - damping dampingRatio: CGFloat, - initialVelocity velocity: CGFloat, - options: UIView.AnimationOptions = [], - animations: @escaping () -> Void - ) -> Self { - self.animations.append( - AnimateSpring(duration: duration, dampingRatio: dampingRatio, initialVelocity: velocity, changes: animations) - .delayed(delay) - .options(options) - ) - return self - } - - /// Adds an ‘extra’ step where preparations or side-effects can be handled. Comparable to a 0-duration animation, without actually being - /// animated in a `UIView` animation closure. - /// - Parameter delay: Amount of time (in seconds) the handler should wait to be executed - /// - Parameter handler: Closure exectured at the specific time in the sequence - /// - Returns: Returns `Self`, enabling the use of chaining mulitple calls - @discardableResult public func extra(delay: TimeInterval = 0, handler: @escaping () -> Void) -> Self { - animations.append( - Extra(perform: handler) - .delayed(delay) - ) - return self - } - - /// Adds an animation sequence to the animation group - /// - /// Adding each part in the group can by done in a chain, as this method returns `Self` - /// - Parameters: - /// - addSteps: Amount of time (in seconds) the animation should last - /// - delay: Amount of time (in seconds) the animation should wait to start - /// - options: Options to use for the animation - /// - timingFunction: `CAMediaTimingFunction` to use for animation - /// - animations: Closure in which values to animate should be changed - /// - Returns: Returns `Self`, enabling the use of chaining mulitple calls - @discardableResult public func animateSteps(_ addSteps: (AnimationSequence) -> Void) -> Self { - let sequence = AnimationSequence() - addSteps(sequence) - animations.append ( - Sequence(delay: 0, animations: sequence.steps) - ) - return self - } - } -} - - -extension AnimationSequence { - - /// Adds a group of animations, all of which will be executed add once. - /// - Parameter addAnimations: Closure used to add animations to the provided `Group` object - /// - Returns: Returns `Self`, enabling the use of chaining mulitple calls - @discardableResult public func addGroup(with addAnimations: (SequenceGroup) -> Void) -> Self { - let group = SequenceGroup() - addAnimations(group) - steps.append( - Group(animations: group.animations) - ) - return self - } -} - -extension StepAnimatable { - - public static func animateSteps(_ addSteps: (AnimationSequence) -> Void, completion: ((Bool) -> Void)? = nil) { - let sequence = AnimationSequence() - - // Call the block with the sequence object, - // hopefully resulting in steps added to the sequence - addSteps(sequence) - - let runningSequence = RunningSequence(animations: sequence.steps) - runningSequence.onComplete { finished in - completion?(finished) - } - runningSequence.animate() - } - - public static func animateGroup(_ addAnimations: (AnimationSequence.SequenceGroup) -> Void, completion: ((Bool) -> Void)? = nil) { - - let group = AnimationSequence.SequenceGroup() - addAnimations(group) - - let runningSequence = RunningSequence(animations: [Group(animations: group.animations)]) - runningSequence.onComplete { finished in - completion?(finished) + /// - Parameters: + /// - animations: Add each animation using this closure. Animation added to a group should conform to ``GroupAnimatable``. + /// - Returns: Instance of ``RunningSequence`` to keep track of and stop animations + @discardableResult + public static func group( + @AnimationBuilder animations builder: () -> [GroupAnimatable] + ) -> RunningSequence { + plan { + Group(animations: builder) } - runningSequence.animate() } } - -/// Applying ``StepAnimatable`` to `UIView` -@available(*, deprecated, message: "Use `AnimationPlanner.plan` instead") -extension UIView: StepAnimatable { } diff --git a/Sources/AnimationPlanner/Animations/Animate.swift b/Sources/AnimationPlanner/Animations/Animate.swift new file mode 100644 index 0000000..9d3d739 --- /dev/null +++ b/Sources/AnimationPlanner/Animations/Animate.swift @@ -0,0 +1,59 @@ +import UIKit + +/// Performs an animation with the provided duration in seconds. Includes properties to set `UIView.AnimationOptions` and +/// even a `CAMediaTimingFunction` to apply to the interpolation of the animated values changed in the ``changes`` closure. +public struct Animate: Animation, SequenceAnimatable, GroupAnimatable { + public let duration: TimeInterval + + public internal(set) var changes: () -> Void + public internal(set) var options: UIView.AnimationOptions? + public internal(set) var timingFunction: CAMediaTimingFunction? + + /// Creates a new animation, animating the properties updated in the ``changes`` closure + /// + /// Only the `duration` parameter is required, all other properties can be added or modified using ``AnimationModifiers``. + /// + /// - Tip: AnimationPlanner provides numerous animation curves through a `CAMediaTimingFunction` extension. + /// Type a period for the `timingFunction` parameter to see what is readily available. Have you tried `.quintOut` yet? + /// + /// - Parameters: + /// - duration: Duration of animation, measured in seconds + /// - timingFunction: Optional `CAMediaTimingFunction` to interpolate animated values with. + /// - changes: Closure executed when the animation is performed + public init( + duration: TimeInterval, + timingFunction: CAMediaTimingFunction? = nil, + changes: @escaping () -> Void = {} + ) { + self.duration = duration + self.timingFunction = timingFunction + self.changes = changes + } +} + +extension Animate: PerformsAnimations { + public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) { + let timing = timingParameters(leadingDelay: leadingDelay) + let createAnimations: (((Bool) -> Void)?) -> Void = { completion in + UIView.animate( + withDuration: timing.duration, + delay: timing.delay, + options: options ?? [], + animations: changes, + completion: completion + ) + } + + if let timingFunction = timingFunction { + CATransaction.begin() + CATransaction.setAnimationDuration(duration) + CATransaction.setAnimationTimingFunction(timingFunction) + + createAnimations(completion) + + CATransaction.commit() + } else { + createAnimations(completion) + } + } +} diff --git a/Sources/AnimationPlanner/Animations/AnimateDelayed.swift b/Sources/AnimationPlanner/Animations/AnimateDelayed.swift new file mode 100644 index 0000000..0bf739c --- /dev/null +++ b/Sources/AnimationPlanner/Animations/AnimateDelayed.swift @@ -0,0 +1,56 @@ +import UIKit + +/// Performs an animation after a delay, only to be used in a context where other animations are run simultaneously +public struct AnimateDelayed: AnimationContainer, DelayedAnimatable, GroupAnimatable { + + public internal(set) var animation: Delayed + + public var duration: TimeInterval { + return delay + originalDuration + } + + public var originalDuration: TimeInterval { + if let delayed = animation as? DelayedAnimatable { + return delayed.originalDuration + } + return animation.duration + } + + public let delay: TimeInterval + + internal init(delay: TimeInterval, animation: Delayed) { + self.animation = animation + self.delay = delay + } +} + +extension AnimateDelayed where Delayed: DelayedAnimatable { + public var duration: TimeInterval { + delay + animation.originalDuration + } +} + +extension AnimateDelayed where Delayed == Animate { + /// Adds a delay to your animation. Can only be added in a ``Group`` context where animations should be performed simultaneously. + /// - Parameters: + /// - delay: Delay in seconds to add to your animation + /// - duration: Duration of animation, measured in seconds + /// - changes: Closure executed when the animation is performed + public init( + delay: TimeInterval, + duration: TimeInterval, + changes: @escaping () -> Void = {} + ) { + let animation = Animate(duration: duration, changes: changes) + self.init(delay: delay, animation: animation) + } +} + +extension AnimateDelayed: Animation where Delayed: Animation { } +extension AnimateDelayed: SpringAnimatable where Delayed: SpringAnimatable { } + +extension AnimateDelayed: PerformsAnimations where Contained: PerformsAnimations { + public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) { + animation.animate(delay: delay + leadingDelay, completion: completion) + } +} diff --git a/Sources/AnimationPlanner/Animations/AnimateSpring.swift b/Sources/AnimationPlanner/Animations/AnimateSpring.swift new file mode 100644 index 0000000..e95d2b4 --- /dev/null +++ b/Sources/AnimationPlanner/Animations/AnimateSpring.swift @@ -0,0 +1,56 @@ +import UIKit + +/// Performs an animation with spring dampening applied, using the same values as UIView spring animations +public struct AnimateSpring: SpringAnimatable, AnimationContainer, GroupAnimatable { + + public internal(set) var animation: Springed + + public let dampingRatio: CGFloat + public let initialVelocity: CGFloat + + internal init(dampingRatio: CGFloat, initialVelocity: CGFloat, animation: Springed) { + self.animation = animation + self.dampingRatio = dampingRatio + self.initialVelocity = initialVelocity + } +} + +extension AnimateSpring where Springed == Animate { + /// Creates a spring-based animation with the expected damping and velocity values. + /// - Parameters: + /// - damping: Value between 0 and 1, same as damping ratio used for `UIView`-based spring animations + /// - initialVelocity: Relative velocity of animation, defined as full extend of animation per second + /// - duration: Duration of animation, measured in seconds + /// - changes: Closure executed when the animation is performed + public init( + duration: TimeInterval, + dampingRatio: CGFloat, + initialVelocity: CGFloat = 0, + changes: @escaping () -> Void = {} + ) { + let animation = Animate(duration: duration, changes: changes) + self.init(dampingRatio: dampingRatio, initialVelocity: initialVelocity, animation: animation) + } +} + +extension AnimateSpring: SequenceAnimatable, SequenceConvertible where Contained: SequenceAnimatable { + public func asSequence() -> [SequenceAnimatable] { [self] } +} + +extension AnimateSpring: Animation where Springed: Animation { } +extension AnimateSpring: DelayedAnimatable where Springed: DelayedAnimatable { } + +extension AnimateSpring: PerformsAnimations { + public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) { + let timing = timingParameters(leadingDelay: leadingDelay) + UIView.animate( + withDuration: timing.duration, + delay: timing.delay, + usingSpringWithDamping: dampingRatio, + initialSpringVelocity: initialVelocity, + options: animation.options ?? [], + animations: animation.changes, + completion: completion + ) + } +} diff --git a/Sources/AnimationPlanner/Animations/AnimationBuilder.swift b/Sources/AnimationPlanner/Animations/AnimationBuilder.swift new file mode 100644 index 0000000..474c2c1 --- /dev/null +++ b/Sources/AnimationPlanner/Animations/AnimationBuilder.swift @@ -0,0 +1,44 @@ +/// Result builder through which either sequence or group animations can be created. Add `@AnimationBuilder` to a closure or method to provide your own animations. +/// The result of your builder function should be an `Array` of either ``SequenceAnimatable`` or ``GroupAnimatable``. +@resultBuilder +public struct AnimationBuilder { } + +extension AnimationBuilder { + public static func buildBlock(_ components: SequenceConvertible...) -> [SequenceAnimatable] { + components.flatMap { $0.asSequence() } + } + + public static func buildArray(_ components: [SequenceConvertible]) -> [SequenceAnimatable] { + components.flatMap { $0.asSequence() } + } + + public static func buildOptional(_ component: SequenceConvertible?) -> [SequenceAnimatable] { + component.map { $0.asSequence() } ?? [] + } + public static func buildEither(first component: SequenceConvertible) -> [SequenceAnimatable] { + component.asSequence() + } + public static func buildEither(second component: SequenceConvertible) -> [SequenceAnimatable] { + component.asSequence() + } +} + +extension AnimationBuilder { + public static func buildBlock(_ components: GroupConvertible...) -> [GroupAnimatable] { + components.flatMap { $0.asGroup() } + } + + public static func buildArray(_ components: [GroupConvertible]) -> [GroupAnimatable] { + components.flatMap { $0.asGroup() } + } + + public static func buildOptional(_ component: GroupConvertible?) -> [GroupAnimatable] { + component.map { $0.asGroup() } ?? [] + } + public static func buildEither(first component: GroupConvertible) -> [GroupAnimatable] { + component.asGroup() + } + public static func buildEither(second component: GroupConvertible) -> [GroupAnimatable] { + component.asGroup() + } +} diff --git a/Sources/AnimationPlanner/Animations/Animations.swift b/Sources/AnimationPlanner/Animations/Animations.swift deleted file mode 100644 index 25fcbf9..0000000 --- a/Sources/AnimationPlanner/Animations/Animations.swift +++ /dev/null @@ -1,189 +0,0 @@ -import UIKit - -/// Performs an animation with the provided duration in seconds. Includes properties to set `UIView.AnimationOptions` and -/// even a `CAMediaTimingFunction` to apply to the interpolation of the animated values changed in the ``changes`` closure. -public struct Animate: Animation, SequenceAnimatable, GroupAnimatable { - public let duration: TimeInterval - - public internal(set) var changes: () -> Void - public internal(set) var options: UIView.AnimationOptions? - public internal(set) var timingFunction: CAMediaTimingFunction? - - /// Creates a new animation, animating the properties updated in the ``changes`` closure - /// - /// Only the `duration` parameter is required, all other properties can be added or modified using ``AnimationModifiers``. - /// - /// - Tip: AnimationPlanner provides numerous animation curves through a `CAMediaTimingFunction` extension. - /// Type a period for the `timingFunction` parameter to see what is readily available. Have you tried `.quintOut` yet? - /// - /// - Parameters: - /// - duration: Duration of animation, measured in seconds - /// - timingFunction: Optional `CAMediaTimingFunction` to interpolate animated values with. - /// - changes: Closure executed when the animation is performed - public init( - duration: TimeInterval, - timingFunction: CAMediaTimingFunction? = nil, - changes: @escaping () -> Void = {} - ) { - self.duration = duration - self.timingFunction = timingFunction - self.changes = changes - } -} - -/// Pauses the sequence for the given amount of seconds before performing the next animation. -public struct Wait: SequenceAnimatable { - public let duration: TimeInterval - - public init(_ duration: TimeInterval) { - self.duration = duration - } -} - -/// Perfoms the provided handler in between your actual animations. -/// Typically used for setting up state before an animation or creating side-effects like haptic feedback. -public struct Extra: SequenceAnimatable, GroupAnimatable { - public let duration: TimeInterval = 0 - - public var perform: () -> Void - public init(perform: @escaping () -> Void) { - self.perform = perform - } -} - -// MARK: - Container - -/// Adds custom behaviour on top of any contained animation. Forwards all required ``Animation`` properties -/// to the contained animation when necessary. -public protocol AnimationContainer { - /// Animation type contained by ``AnimationContainer`` - associatedtype Contained: Animatable - /// Animation contained any animation using ``AnimationContainer``. - var animation: Contained { get } -} - -/// Forwarding ``Animation`` properties -extension AnimationContainer where Contained: Animation { - /// Forwarded ``Animation`` property for ``Animate/duration`` - public var duration: TimeInterval { animation.duration } - /// Forwarded ``Animation`` property for ``Animation/changes`` - public var changes: () -> Void { animation.changes } - /// Forwarded ``Animation`` property for ``Animation/options`` - public var options: UIView.AnimationOptions? { animation.options } - /// Forwarded ``Animation`` property for ``Animation/timingFunction`` - public var timingFunction: CAMediaTimingFunction? { animation.timingFunction } -} - -/// Forwarding ``DelayedAnimatable`` properties -extension AnimationContainer where Contained: DelayedAnimatable { - /// Forwarded ``DelayedAnimatable`` property for ``DelayedAnimatable/delay`` - public var delay: TimeInterval { - animation.delay - } - - /// Forwarded ``DelayedAnimatable`` property for ``DelayedAnimatable/originalDuration`` - public var originalDuration: TimeInterval { - animation.originalDuration - } -} - -/// Forwarding ``SpringAnimatable`` properties -extension AnimationContainer where Contained: SpringAnimatable { - /// Forwarded ``SpringAnimatable`` property for ``SpringAnimatable/dampingRatio`` - public var dampingRatio: CGFloat { animation.dampingRatio } - /// Forwarded ``SpringAnimatable`` property for ``SpringAnimatable/initialVelocity`` - public var initialVelocity: CGFloat { animation.initialVelocity } -} - -// MARK: - Spring - -/// Performs an animation with spring dampening applied, using the same values as UIView spring animations -public struct AnimateSpring: SpringAnimatable, AnimationContainer, GroupAnimatable { - - public internal(set) var animation: Springed - - public let dampingRatio: CGFloat - public let initialVelocity: CGFloat - - internal init(dampingRatio: CGFloat, initialVelocity: CGFloat, animation: Springed) { - self.animation = animation - self.dampingRatio = dampingRatio - self.initialVelocity = initialVelocity - } -} - -extension AnimateSpring where Springed == Animate { - /// Creates a spring-based animation with the expected damping and velocity values. - /// - Parameters: - /// - damping: Value between 0 and 1, same as damping ratio used for `UIView`-based spring animations - /// - initialVelocity: Relative velocity of animation, defined as full extend of animation per second - /// - duration: Duration of animation, measured in seconds - /// - changes: Closure executed when the animation is performed - public init( - duration: TimeInterval, - dampingRatio: CGFloat, - initialVelocity: CGFloat = 0, - changes: @escaping () -> Void = {} - ) { - let animation = Animate(duration: duration, changes: changes) - self.init(dampingRatio: dampingRatio, initialVelocity: initialVelocity, animation: animation) - } -} - -extension AnimateSpring: SequenceAnimatable, SequenceConvertible where Contained: SequenceAnimatable { - public func asSequence() -> [SequenceAnimatable] { [self] } -} - -extension AnimateSpring: Animation where Springed: Animation { } -extension AnimateSpring: DelayedAnimatable where Springed: DelayedAnimatable { } - -// MARK: - Delay - -/// Performs an animation after a delay, only to be used in a context where other animations are run simultaneously -public struct AnimateDelayed: AnimationContainer, DelayedAnimatable, GroupAnimatable { - - public internal(set) var animation: Delayed - - public var duration: TimeInterval { - return delay + originalDuration - } - - public var originalDuration: TimeInterval { - if let delayed = animation as? DelayedAnimatable { - return delayed.originalDuration - } - return animation.duration - } - - public let delay: TimeInterval - - internal init(delay: TimeInterval, animation: Delayed) { - self.animation = animation - self.delay = delay - } -} - -extension AnimateDelayed where Delayed: DelayedAnimatable { - public var duration: TimeInterval { - delay + animation.originalDuration - } -} - -extension AnimateDelayed where Delayed == Animate { - /// Adds a delay to your animation. Can only be added in a ``Group`` context where animations should be performed simultaneously. - /// - Parameters: - /// - delay: Delay in seconds to add to your animation - /// - duration: Duration of animation, measured in seconds - /// - changes: Closure executed when the animation is performed - public init( - delay: TimeInterval, - duration: TimeInterval, - changes: @escaping () -> Void = {} - ) { - let animation = Animate(duration: duration, changes: changes) - self.init(delay: delay, animation: animation) - } -} - -extension AnimateDelayed: Animation where Delayed: Animation { } -extension AnimateDelayed: SpringAnimatable where Delayed: SpringAnimatable { } diff --git a/Sources/AnimationPlanner/Animations/Builder.swift b/Sources/AnimationPlanner/Animations/Builder.swift deleted file mode 100644 index a98cedb..0000000 --- a/Sources/AnimationPlanner/Animations/Builder.swift +++ /dev/null @@ -1,125 +0,0 @@ -import UIKit - -/// Result builder through which either sequence or group animations can be created. Add `@AnimationBuilder` to a closure or method to provide your own animations. -/// The result of your builder function should be an `Array` of either ``SequenceAnimatable`` or ``GroupAnimatable``. -@resultBuilder -public struct AnimationBuilder { } - -/// Chain multiple `UIView` animations with a clear declarative syntax, describing each step along the way. -/// Start by typing `AnimationPlanner.plan` and provide all of your animations from the `animations` closure. -/// -/// Begin planning your animation by using either of the following static methods: -/// - ``plan(animations:)`` start a sequence animation where all animations are performed in order. -/// - ``group(animations:)`` start a group animation where all animations are performed simultaneously. -/// -/// - Tip: To get started, read and get up to speed on how to use AnimationPlanner, -/// or go through the whole documentation on ``AnimationPlanner`` to get an overview of all the available functionalities. -public struct AnimationPlanner { - - /// Start a new animation sequence where animations added will be performed in order, meaning a subsequent animation starts right after the previous finishes. - /// - /// ```swift - /// AnimationPlanner.plan { - /// Animate(duration: 0.25) { view.backgroundColor = .systemRed } - /// Wait(0.5) - /// Animate(duration: 0.5) { - /// view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) - /// }.spring(damping: 0.68) - /// } - /// ``` - /// - Parameters: - /// - animations: Add each animation using this closure. Animation added to a sequence should conform to ``GroupAnimatable``. - /// - Returns: Instance of ``RunningSequence`` to keep track of and stop animations - @discardableResult - public static func plan( - @AnimationBuilder animations builder: () -> [SequenceAnimatable] - ) -> RunningSequence { - RunningSequence(animations: builder()) - .animate() - } - - /// Start a new group animation where animations added will be performed simultaneously, meaning all animations run at the same time. - /// - /// ```swift - /// AnimationPlanner.group { - /// Animate(duration: 0.5) { - /// view.frame.origin.y = 0 - /// }.delayed(0.15) - /// Animate(duration: 0.3) { - /// view.backgroundColor = .systemBlue - /// }.delayed(0.2) - /// } - /// ``` - /// - /// - Parameters: - /// - animations: Add each animation using this closure. Animation added to a group should conform to ``GroupAnimatable``. - /// - Returns: Instance of ``RunningSequence`` to keep track of and stop animations - @discardableResult - public static func group( - @AnimationBuilder animations builder: () -> [GroupAnimatable] - ) -> RunningSequence { - plan { - Group(animations: builder) - } - } -} - -// MARK: - Building sequences - -/// Provides a way to create a uniform sequence from all animations conforming to ``SequenceAnimatable`` -public protocol SequenceConvertible { - func asSequence() -> [SequenceAnimatable] -} - -/// Provides a way to group toghether animations conforming to ``GroupAnimatable`` -public protocol GroupConvertible { - func asGroup() -> [GroupAnimatable] -} - -extension Array: SequenceConvertible where Element == SequenceAnimatable { - public func asSequence() -> [SequenceAnimatable] { flatMap { $0.asSequence() } } -} - -extension Array: GroupConvertible where Element == GroupAnimatable { - public func asGroup() -> [GroupAnimatable] { flatMap { $0.asGroup() } } -} - -extension AnimationBuilder { - public static func buildBlock(_ components: SequenceConvertible...) -> [SequenceAnimatable] { - components.flatMap { $0.asSequence() } - } - - public static func buildArray(_ components: [SequenceConvertible]) -> [SequenceAnimatable] { - components.flatMap { $0.asSequence() } - } - - public static func buildOptional(_ component: SequenceConvertible?) -> [SequenceAnimatable] { - component.map { $0.asSequence() } ?? [] - } - public static func buildEither(first component: SequenceConvertible) -> [SequenceAnimatable] { - component.asSequence() - } - public static func buildEither(second component: SequenceConvertible) -> [SequenceAnimatable] { - component.asSequence() - } -} - -extension AnimationBuilder { - public static func buildBlock(_ components: GroupConvertible...) -> [GroupAnimatable] { - components.flatMap { $0.asGroup() } - } - - public static func buildArray(_ components: [GroupConvertible]) -> [GroupAnimatable] { - components.flatMap { $0.asGroup() } - } - - public static func buildOptional(_ component: GroupConvertible?) -> [GroupAnimatable] { - component.map { $0.asGroup() } ?? [] - } - public static func buildEither(first component: GroupConvertible) -> [GroupAnimatable] { - component.asGroup() - } - public static func buildEither(second component: GroupConvertible) -> [GroupAnimatable] { - component.asGroup() - } -} diff --git a/Sources/AnimationPlanner/Animations/Extra.swift b/Sources/AnimationPlanner/Animations/Extra.swift new file mode 100644 index 0000000..deeb009 --- /dev/null +++ b/Sources/AnimationPlanner/Animations/Extra.swift @@ -0,0 +1,30 @@ +import UIKit + +/// Perfoms the provided handler in between your actual animations. +/// Typically used for setting up state before an animation or creating side-effects like haptic feedback. +public struct Extra: SequenceAnimatable, GroupAnimatable { + public let duration: TimeInterval = 0 + + public var perform: () -> Void + public init(perform: @escaping () -> Void) { + self.perform = perform + } +} + +extension Extra: PerformsAnimations { + public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) { + let timing = timingParameters(leadingDelay: leadingDelay) + + let animation: () -> Void = { + self.perform() + completion?(true) + } + guard timing.delay > 0 else { + animation() + return + } + DispatchQueue.main.asyncAfter(deadline: .now() + timing.delay) { + animation() + } + } +} diff --git a/Sources/AnimationPlanner/Animations/Loop.swift b/Sources/AnimationPlanner/Animations/Loop.swift index 31de052..c3f2b1c 100644 --- a/Sources/AnimationPlanner/Animations/Loop.swift +++ b/Sources/AnimationPlanner/Animations/Loop.swift @@ -101,7 +101,7 @@ extension Loop: GroupConvertible where Looped == GroupAnimatable { } } -extension Loop where Looped == GroupAnimatable { +extension Loop: PerformsAnimations where Looped == GroupAnimatable { public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) { Group(animations: animations).animate(delay: leadingDelay, completion: completion) } diff --git a/Sources/AnimationPlanner/Animations/PerformAnimations.swift b/Sources/AnimationPlanner/Animations/PerformAnimations.swift deleted file mode 100644 index 2fa1295..0000000 --- a/Sources/AnimationPlanner/Animations/PerformAnimations.swift +++ /dev/null @@ -1,113 +0,0 @@ -import UIKit - -/* - * -- NOT USED YET, WILL BE IN PHASE 2 -- - */ - -/// Creates actual `UIView` animations for all animation structs -public protocol PerformsAnimations { - /// Perform the actual animation - /// - Parameters: - /// - delay: Any delay accumulated (from preceding ``Wait`` structs) leading up to the animation. - /// Waits for this amount of seconds before actually performing the animation - /// - completion: Optional closure called when animation completes - func animate(delay leadingDelay: TimeInterval, completion: ((_ finished: Bool) -> Void)?) -} - -internal protocol ActuallyPerformsAnimations { - func prepareAnimation(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) - func performAnimations(delay totalDelay: TimeInterval, duration: TimeInterval, completion: ((Bool) -> Void)?) -} - -extension ActuallyPerformsAnimations where Self: Animatable { - - func prepareAnimation(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)? = nil) { - var duration = self.duration - var delay = leadingDelay - - if let delayed = self as? DelayedAnimatable { - duration = delayed.originalDuration - delay += delayed.delay - } - - self.performAnimations(delay: delay, duration: duration, completion: completion) - } -} - -extension Animate: PerformsAnimations { - public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) { - prepareAnimation(delay: leadingDelay, completion: completion) - } -} -extension Animate: ActuallyPerformsAnimations { - func performAnimations(delay totalDelay: TimeInterval, duration: TimeInterval, completion: ((Bool) -> Void)? = nil) { - let createAnimations: (((Bool) -> Void)?) -> Void = { completion in - UIView.animate( - withDuration: duration, - delay: totalDelay, - options: options ?? [], - animations: changes, - completion: completion - ) - } - - if let timingFunction = timingFunction { - CATransaction.begin() - CATransaction.setAnimationDuration(duration) - CATransaction.setAnimationTimingFunction(timingFunction) - - createAnimations(completion) - - CATransaction.commit() - } else { - createAnimations(completion) - } - } -} - -extension Extra: PerformsAnimations { - public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) { - prepareAnimation(delay: leadingDelay, completion: completion) - } -} - -extension Extra: ActuallyPerformsAnimations { - func performAnimations(delay totalDelay: TimeInterval, duration: TimeInterval, completion: ((Bool) -> Void)? = nil) { - let animation: () -> Void = { - self.perform() - completion?(true) - } - guard totalDelay > 0 else { - animation() - return - } - DispatchQueue.main.asyncAfter(deadline: .now() + totalDelay) { - animation() - } - } -} - -extension AnimateSpring: PerformsAnimations { - public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) { - prepareAnimation(delay: leadingDelay, completion: completion) - } -} -extension AnimateSpring: ActuallyPerformsAnimations { - func performAnimations(delay totalDelay: TimeInterval, duration: TimeInterval, completion: ((Bool) -> Void)? = nil) { - UIView.animate( - withDuration: duration, - delay: totalDelay, - usingSpringWithDamping: dampingRatio, - initialSpringVelocity: initialVelocity, - options: animation.options ?? [], - animations: animation.changes, - completion: completion - ) - } -} - -extension AnimateDelayed: PerformsAnimations where Contained: PerformsAnimations { - public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) { - animation.animate(delay: delay + leadingDelay, completion: completion) - } -} diff --git a/Sources/AnimationPlanner/Animations/Wait.swift b/Sources/AnimationPlanner/Animations/Wait.swift new file mode 100644 index 0000000..3da59e5 --- /dev/null +++ b/Sources/AnimationPlanner/Animations/Wait.swift @@ -0,0 +1,10 @@ +import UIKit + +/// Pauses the sequence for the given amount of seconds before performing the next animation. +public struct Wait: SequenceAnimatable { + public let duration: TimeInterval + + public init(_ duration: TimeInterval) { + self.duration = duration + } +} diff --git a/Sources/AnimationPlanner/Animations/Animates.swift b/Sources/AnimationPlanner/Protocols/Animatable.swift similarity index 87% rename from Sources/AnimationPlanner/Animations/Animates.swift rename to Sources/AnimationPlanner/Protocols/Animatable.swift index 9b83e52..875335f 100644 --- a/Sources/AnimationPlanner/Animations/Animates.swift +++ b/Sources/AnimationPlanner/Protocols/Animatable.swift @@ -1,12 +1,12 @@ import UIKit -/// Anything that can be animated in AnimationPlanner +/// Anything that can be used to create animations in AnimationPlanner public protocol Animatable { /// Full duration of the animation var duration: TimeInterval { get } } -/// Actual animation that can be used to construct `UIView` animations +/// Animation that can be used to construct `UIView` animations public protocol Animation: Animatable, PerformsAnimations { /// Changes on views to perform animation with var changes: () -> Void { get } @@ -30,7 +30,7 @@ extension GroupAnimatable { public func asGroup() -> [GroupAnimatable] { [self] } } -/// Adds a delaying functionality to an animation. Delayed animations can only be added in a grouped context, where each animation is performed simultaneously. Adding a delay to a sequence animation can be done by preceding it with a ``Wait`` struct. +/// Adds delaying functionality to an animation. Delayed animations can only be added in a ``Group`` context, where each animation is performed simultaneously. Adding a delay to a sequence animation can be done by preceding it with a ``Wait`` struct. public protocol DelayedAnimatable: GroupAnimatable { /// Delay in seconds after which the animation should start var delay: TimeInterval { get } @@ -38,7 +38,7 @@ public protocol DelayedAnimatable: GroupAnimatable { var originalDuration: TimeInterval { get } } -/// Performs an animation with spring-based parameters +/// Adds spring-based animation parameters to an animation. public protocol SpringAnimatable: Animatable { /// Spring damping used for spring-based animation. To quote `UIView`’s animate documentation: /// “To smoothly decelerate the animation without oscillation, use a value of 1. Employ a damping ratio closer to zero to increase oscillation.” diff --git a/Sources/AnimationPlanner/Protocols/AnimationContainer.swift b/Sources/AnimationPlanner/Protocols/AnimationContainer.swift new file mode 100644 index 0000000..b18b5ea --- /dev/null +++ b/Sources/AnimationPlanner/Protocols/AnimationContainer.swift @@ -0,0 +1,43 @@ +import UIKit + +/// Adds custom behaviour on top of any contained animation. Forwards all required ``Animation`` properties +/// to the contained animation when necessary. +public protocol AnimationContainer { + /// Animation type contained by ``AnimationContainer`` + associatedtype Contained: Animatable + /// Animation contained any animation using ``AnimationContainer``. + var animation: Contained { get } +} + +/// Forwarding ``Animation`` properties +extension AnimationContainer where Contained: Animation { + /// Forwarded ``Animation`` property for ``Animate/duration`` + public var duration: TimeInterval { animation.duration } + /// Forwarded ``Animation`` property for ``Animation/changes`` + public var changes: () -> Void { animation.changes } + /// Forwarded ``Animation`` property for ``Animation/options`` + public var options: UIView.AnimationOptions? { animation.options } + /// Forwarded ``Animation`` property for ``Animation/timingFunction`` + public var timingFunction: CAMediaTimingFunction? { animation.timingFunction } +} + +/// Forwarding ``DelayedAnimatable`` properties +extension AnimationContainer where Contained: DelayedAnimatable { + /// Forwarded ``DelayedAnimatable`` property for ``DelayedAnimatable/delay`` + public var delay: TimeInterval { + animation.delay + } + + /// Forwarded ``DelayedAnimatable`` property for ``DelayedAnimatable/originalDuration`` + public var originalDuration: TimeInterval { + animation.originalDuration + } +} + +/// Forwarding ``SpringAnimatable`` properties +extension AnimationContainer where Contained: SpringAnimatable { + /// Forwarded ``SpringAnimatable`` property for ``SpringAnimatable/dampingRatio`` + public var dampingRatio: CGFloat { animation.dampingRatio } + /// Forwarded ``SpringAnimatable`` property for ``SpringAnimatable/initialVelocity`` + public var initialVelocity: CGFloat { animation.initialVelocity } +} diff --git a/Sources/AnimationPlanner/Protocols/AnimationConvertible.swift b/Sources/AnimationPlanner/Protocols/AnimationConvertible.swift new file mode 100644 index 0000000..1f88f9d --- /dev/null +++ b/Sources/AnimationPlanner/Protocols/AnimationConvertible.swift @@ -0,0 +1,17 @@ +/// Provides a way to create a uniform sequence from all animations conforming to ``SequenceAnimatable`` +public protocol SequenceConvertible { + func asSequence() -> [SequenceAnimatable] +} + +/// Provides a way to group toghether animations conforming to ``GroupAnimatable`` +public protocol GroupConvertible { + func asGroup() -> [GroupAnimatable] +} + +extension Array: SequenceConvertible where Element == SequenceAnimatable { + public func asSequence() -> [SequenceAnimatable] { flatMap { $0.asSequence() } } +} + +extension Array: GroupConvertible where Element == GroupAnimatable { + public func asGroup() -> [GroupAnimatable] { flatMap { $0.asGroup() } } +} diff --git a/Sources/AnimationPlanner/Animations/Modifiers.swift b/Sources/AnimationPlanner/Protocols/AnimationModifiers.swift similarity index 99% rename from Sources/AnimationPlanner/Animations/Modifiers.swift rename to Sources/AnimationPlanner/Protocols/AnimationModifiers.swift index 4a28a3e..1d1967a 100644 --- a/Sources/AnimationPlanner/Animations/Modifiers.swift +++ b/Sources/AnimationPlanner/Protocols/AnimationModifiers.swift @@ -41,7 +41,8 @@ extension Animate: AnimationModifiers { } public func changes(_ changes: @escaping () -> Void) -> Animate { mutate { $0.changes = changes } - }} + } +} // MARK: - Spring modifiers diff --git a/Sources/AnimationPlanner/Protocols/PerformsAnimations.swift b/Sources/AnimationPlanner/Protocols/PerformsAnimations.swift new file mode 100644 index 0000000..328114a --- /dev/null +++ b/Sources/AnimationPlanner/Protocols/PerformsAnimations.swift @@ -0,0 +1,32 @@ +import UIKit + +/// Creates actual `UIView` animations for all animation structs. Implement ``animate(delay:completion:)`` to make sure any custom animation creates an actual animation. +/// Use the default implementation of ``timingParameters(leadingDelay:)-2swvd`` to get the most accurate timing parameters for your animation so any set delay isn't missed. +public protocol PerformsAnimations { + /// Perform the actual animation + /// - Parameters: + /// - delay: Any delay accumulated (from preceding ``Wait`` structs) leading up to the animation. + /// Waits for this amount of seconds before actually performing the animation + /// - completion: Optional closure called when animation completes + func animate(delay leadingDelay: TimeInterval, completion: ((_ finished: Bool) -> Void)?) + + /// Queries the animation and possible contained animations to find the correct timing values to use to create an actual animation + /// - Parameter leadingDelay: Delay to add before performing animation + /// - Returns: Tuple containing a delay and duration in seconds + func timingParameters(leadingDelay: TimeInterval) -> (delay: TimeInterval, duration: TimeInterval) +} + +extension PerformsAnimations { + + public func timingParameters(leadingDelay: TimeInterval) -> (delay: TimeInterval, duration: TimeInterval) { + var parameters = (delay: leadingDelay, duration: TimeInterval(0)) + + if let delayed = self as? DelayedAnimatable { + parameters.delay += delayed.delay + parameters.duration = delayed.originalDuration + } else if let animation = self as? Animatable { + parameters.duration = animation.duration + } + return parameters + } +} diff --git a/Sources/AnimationPlanner/Animations/RunningSequence.swift b/Sources/AnimationPlanner/RunningSequence.swift similarity index 100% rename from Sources/AnimationPlanner/Animations/RunningSequence.swift rename to Sources/AnimationPlanner/RunningSequence.swift diff --git a/Tests/AnimationPlannerTests/AnimationPlannerTests.swift b/Tests/AnimationPlannerTests/AnimationPlannerTests.swift index f63d85a..24847dc 100644 --- a/Tests/AnimationPlannerTests/AnimationPlannerTests.swift +++ b/Tests/AnimationPlannerTests/AnimationPlannerTests.swift @@ -1,5 +1,6 @@ import UIKit import XCTest +import AnimationPlanner class AnimationPlannerTests: XCTestCase { @@ -19,7 +20,7 @@ class AnimationPlannerTests: XCTestCase { view = nil } - /// Runs your animation logic, waits for completion and fails when expected duration varies from provided duration (allowing for precision) + /// Runs your animation logic, waits for completion and fails when expected duration varies from provided duration (allowing for precision). Adds the completion handler to the returned `RunningSequence` object when only using `AnimationPlanner.plan` or `.group`. Othewise use `runAnimationTest` /// - Parameters: /// - duration: Duration of animimation, or total duration of all animation steps, defaults to random duration /// - precision: Precision to use when comparing expected duration and time to complete animations @@ -27,6 +28,20 @@ class AnimationPlannerTests: XCTestCase { /// - completion: Closure to call when animations have completed /// - usedDuration: Duration for animation, use this argument when no specific duration is provided /// - usedPrecision: Precision for duration check, use this argument when no specific precision is provided + func runAnimationBuilderTest( + duration: TimeInterval = randomDuration, + precision: TimeInterval = durationPrecision, + _ animations: @escaping ( + _ usedDuration: TimeInterval, + _ usedPrecision: TimeInterval) -> RunningSequence? + ) { + runAnimationTest(duration: duration, precision: precision) { completion, usedDuration, usedPrecision in + let runningSequence = animations(duration, precision) + XCTAssertNotNil(runningSequence) + runningSequence?.onComplete(completion) + } + } + func runAnimationTest( duration: TimeInterval = randomDuration, precision: TimeInterval = durationPrecision, @@ -43,7 +58,7 @@ class AnimationPlannerTests: XCTestCase { assertDifference(startTime: startTime, duration: duration, precision: precision) finishedExpectation.fulfill() } - // perform actual animation stuff + animations(completion, duration, precision) waitForExpectations(timeout: duration + precision * 2) @@ -56,7 +71,6 @@ func assertDifference(startTime: CFTimeInterval, duration: TimeInterval, precisi let finishedTime = CACurrentMediaTime() - startTime let difference = finishedTime - duration XCTAssert(abs(difference) < precision, "unexpected completion time (difference \(difference) seconds (precision \(precision))") - print("** DIFFERENCE: \(difference), (precision: \(precision))") } fileprivate extension CGFloat { diff --git a/Tests/AnimationPlannerTests/BuilderTests.swift b/Tests/AnimationPlannerTests/BuilderTests.swift index d9e5a2c..a322fc4 100644 --- a/Tests/AnimationPlannerTests/BuilderTests.swift +++ b/Tests/AnimationPlannerTests/BuilderTests.swift @@ -98,34 +98,29 @@ class BuilderTests: AnimationPlannerTests { XCTAssertEqual(waitStartingGroup.duration, longestAnimation.totalDuration) XCTAssertEqual(waitStartingGroup.duration, waitEndingGroup.duration) - runAnimationTest(duration: longestAnimation.totalDuration, precision: precision) { completion, _, _ in + runAnimationBuilderTest(duration: longestAnimation.totalDuration, precision: precision) { _, _ in AnimationPlanner.plan { waitStartingGroup - }.onComplete { finished in - completion(finished) } } - runAnimationTest(duration: longestAnimation.totalDuration, precision: precision) { completion, _, _ in + runAnimationBuilderTest(duration: longestAnimation.totalDuration, precision: precision) { _, _ in AnimationPlanner.plan { - waitStartingGroup - }.onComplete { finished in - completion(finished) + waitEndingGroup } } } func testEmptyBuilder() { - runAnimationTest(duration: 0) { completion, _, _ in + runAnimationBuilderTest(duration: 0) { _, _ in AnimationPlanner.plan { Extra { print("👋") } - }.onComplete { finished in - completion(finished) } + } } @@ -134,7 +129,7 @@ class BuilderTests: AnimationPlannerTests { let numberOfSteps: TimeInterval = 3 let duration = totalDuration / numberOfSteps - runAnimationTest(duration: totalDuration) { completion, _, _ in + runAnimationBuilderTest(duration: totalDuration) { _, _ in AnimationPlanner.plan { Animate(duration: duration) { @@ -145,9 +140,8 @@ class BuilderTests: AnimationPlannerTests { self.performRandomAnimation() } .spring(damping: 0.8) - }.onComplete { finished in - completion(finished) } + } } @@ -156,7 +150,7 @@ class BuilderTests: AnimationPlannerTests { let numberOfSteps: TimeInterval = 3 let duration = totalDuration / numberOfSteps - runAnimationTest(duration: totalDuration) { completion, _, _ in + runAnimationBuilderTest(duration: totalDuration) { _, _ in AnimationPlanner.plan { Animate(duration: duration) @@ -169,9 +163,8 @@ class BuilderTests: AnimationPlannerTests { self.performRandomAnimation() } .options(.allowAnimatedContent) - }.onComplete { finished in - completion(finished) } + } } @@ -180,7 +173,7 @@ class BuilderTests: AnimationPlannerTests { let numberOfSteps: TimeInterval = 1 let duration = totalDuration / numberOfSteps - runAnimationTest(duration: totalDuration) { completion, _, _ in + runAnimationBuilderTest(duration: totalDuration) { _, _ in AnimationPlanner.plan { Animate(duration: duration) { @@ -188,88 +181,16 @@ class BuilderTests: AnimationPlannerTests { } .spring(damping: 0.82) .options(.allowUserInteraction) - }.onComplete { finished in - completion(finished) - } - - } - } - - func testGroup() { - let totalDuration: TimeInterval = 5 - let numberOfSteps: TimeInterval = 4 - let duration = totalDuration / numberOfSteps - let delay: TimeInterval = 1 - - runAnimationTest(duration: totalDuration + delay) { completion, _, _ in - - AnimationPlanner.plan { - Animate(duration: duration) { - self.performRandomAnimation() - } - Wait(duration) - Group { - Animate(duration: duration, changes: { - self.performRandomAnimation() - }) - .spring(damping: 0.82) - .delayed(delay / 2) - - Animate(duration: duration) { - self.performRandomAnimation() - } - - Animate(duration: duration, changes: { - self.performRandomAnimation() - }) - .delayed(delay) - .spring(damping: 0.82) - } - Animate(duration: duration) { - self.performRandomAnimation() - } - }.onComplete { finished in - completion(finished) - } - - } - } - - func testMultipleGroups() { - let numberOfGroups = 2 - let numberOfSteps = 2 - let groups = (0..