diff --git a/Sources/AnimationPlanner/AnimationPlanner.swift b/Sources/AnimationPlanner/AnimationPlanner.swift index 14b1686..2de96bf 100644 --- a/Sources/AnimationPlanner/AnimationPlanner.swift +++ b/Sources/AnimationPlanner/AnimationPlanner.swift @@ -27,7 +27,7 @@ public struct AnimationPlanner { /// - Returns: Instance of ``RunningSequence`` to keep track of and stop animations @discardableResult public static func plan( - @AnimationBuilder animations builder: () -> [SequenceAnimatable] + @SequenceBuilder animations builder: () -> [SequenceAnimatable] ) -> RunningSequence { RunningSequence(animations: builder()) .animate() @@ -51,7 +51,7 @@ public struct AnimationPlanner { /// - Returns: Instance of ``RunningSequence`` to keep track of and stop animations @discardableResult public static func group( - @AnimationBuilder animations builder: () -> [GroupAnimatable] + @GroupBuilder animations builder: () -> [GroupAnimatable] ) -> RunningSequence { plan { Group(animations: builder) diff --git a/Sources/AnimationPlanner/Animations/AnimateSpring.swift b/Sources/AnimationPlanner/Animations/AnimateSpring.swift index e95d2b4..7fb3cb0 100644 --- a/Sources/AnimationPlanner/Animations/AnimateSpring.swift +++ b/Sources/AnimationPlanner/Animations/AnimateSpring.swift @@ -34,7 +34,7 @@ extension AnimateSpring where Springed == Animate { } extension AnimateSpring: SequenceAnimatable, SequenceConvertible where Contained: SequenceAnimatable { - public func asSequence() -> [SequenceAnimatable] { [self] } + public func animations() -> [SequenceAnimatable] { [self] } } extension AnimateSpring: Animation where Springed: Animation { } diff --git a/Sources/AnimationPlanner/Animations/AnimationBuilder.swift b/Sources/AnimationPlanner/Animations/AnimationBuilder.swift index 474c2c1..4cd5a6b 100644 --- a/Sources/AnimationPlanner/Animations/AnimationBuilder.swift +++ b/Sources/AnimationPlanner/Animations/AnimationBuilder.swift @@ -1,44 +1,39 @@ /// 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 struct SequenceBuilder { public static func buildBlock(_ components: SequenceConvertible...) -> [SequenceAnimatable] { - components.flatMap { $0.asSequence() } + components.flatMap { $0.animations() } } - public static func buildArray(_ components: [SequenceConvertible]) -> [SequenceAnimatable] { - components.flatMap { $0.asSequence() } + components.flatMap { $0.animations() } } - public static func buildOptional(_ component: SequenceConvertible?) -> [SequenceAnimatable] { - component.map { $0.asSequence() } ?? [] + component.map { $0.animations() } ?? [] } public static func buildEither(first component: SequenceConvertible) -> [SequenceAnimatable] { - component.asSequence() + component.animations() } public static func buildEither(second component: SequenceConvertible) -> [SequenceAnimatable] { - component.asSequence() + component.animations() } } -extension AnimationBuilder { +@resultBuilder +public struct GroupBuilder { public static func buildBlock(_ components: GroupConvertible...) -> [GroupAnimatable] { - components.flatMap { $0.asGroup() } + components.flatMap { $0.animations() } } - public static func buildArray(_ components: [GroupConvertible]) -> [GroupAnimatable] { - components.flatMap { $0.asGroup() } + components.flatMap { $0.animations() } } - public static func buildOptional(_ component: GroupConvertible?) -> [GroupAnimatable] { - component.map { $0.asGroup() } ?? [] + component.map { $0.animations() } ?? [] } public static func buildEither(first component: GroupConvertible) -> [GroupAnimatable] { - component.asGroup() + component.animations() } public static func buildEither(second component: GroupConvertible) -> [GroupAnimatable] { - component.asGroup() + component.animations() } } diff --git a/Sources/AnimationPlanner/Animations/Extra.swift b/Sources/AnimationPlanner/Animations/Extra.swift index deeb009..7a7da00 100644 --- a/Sources/AnimationPlanner/Animations/Extra.swift +++ b/Sources/AnimationPlanner/Animations/Extra.swift @@ -1,6 +1,6 @@ import UIKit -/// Perfoms the provided handler in between your actual animations. +/// Performs 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 diff --git a/Sources/AnimationPlanner/Animations/Group.swift b/Sources/AnimationPlanner/Animations/Group.swift index f0b9d0a..e92c647 100644 --- a/Sources/AnimationPlanner/Animations/Group.swift +++ b/Sources/AnimationPlanner/Animations/Group.swift @@ -20,7 +20,7 @@ public struct Group: SequenceAnimatable { /// Creates a new `Group` providing a way to perform multiple animations simultaneously, meaning all animations run at the same time. /// - Parameter animations: Add each animation from within this closure. Animations added to a group should conform to ``GroupAnimatable``. - public init(@AnimationBuilder animations builder: () -> [GroupAnimatable]) { + public init(@GroupBuilder animations builder: () -> [GroupAnimatable]) { self.init(animations: builder()) } } diff --git a/Sources/AnimationPlanner/Animations/Loop.swift b/Sources/AnimationPlanner/Animations/Loop.swift index c3f2b1c..55e5547 100644 --- a/Sources/AnimationPlanner/Animations/Loop.swift +++ b/Sources/AnimationPlanner/Animations/Loop.swift @@ -1,128 +1,29 @@ -import UIKit +import Foundation +@available( + *, unavailable, + message: "Loop convenience struct has been removed following type checking changes in Swift 5.8. Use a for-loops or the sequence extension method `mapSequence()` or `mapGroup()` instead" +) /// Loop through a sequence or for a specified repeat count to easily repeat multiple animation. -/// -/// Either create a `Loop` with the default initializer where you set `repeatCount` or use the static method ``through(_:animations:)-sb44`` to loop through an existing array. -/// -/// ```swift -/// Loop(for: numberOfLoops) { index in -/// Animate(duration: 0.2) { -/// view.frame.origin += 10 -/// } -/// Wait(0.5) -/// } -/// ``` -public struct Loop { - /// Total duration of loop. Sum of all animations when animated in sequence, duration of longest animation when animated in a group. - public var duration: TimeInterval - /// All animations created in the loop. - public let animations: [Looped] - - fileprivate init( - repeatCount: Int, - @AnimationBuilder builder: (_ index: Int) -> [Looped] - ) { - self.animations = (0..( - _ sequence: S, - @AnimationBuilder with builder: (S.Element) -> [Looped] - ) -> [Looped] { - sequence.flatMap(builder) - } -} - -extension Loop: SequenceConvertible where Looped == SequenceAnimatable { - - public func asSequence() -> [SequenceAnimatable] { - animations - } - - /// Creates a new Loop that repeats for the given amount of times. - /// - Parameters: - /// - repeatCount: How many times the loop should repeat. The index of each loop is provided as a argument in the `animations` closure - /// - animations: Add each animation from within this closure. Animations added to this loop should conform to ``SequenceAnimatable`` +/// - Warning: This struct is no longer available. The same functionality can be achieved by using `for`-loop or the methods `mapSequence()` and `mapGroup()` on any Swift Sequence. +public struct Loop: SequenceAnimatable, GroupAnimatable { + public var duration: TimeInterval = 0 public init( for repeatCount: Int, - @AnimationBuilder animations builder: (_ index: Int) -> [SequenceAnimatable] - ) { - self.init(repeatCount: repeatCount, builder: builder) - } - - /// Loop through a sequence of values, like objects in an array or a range of numbers - /// - Parameters: - /// - sequence: Sequence to loop through, each element will be handled in the `animations` closure - /// - animations: Add each animation from within this closure. Animations added to this loop should conform to ``SequenceAnimatable`` - /// - Returns: Sequence of all animations created in the `animation` closure + @SequenceBuilder animations builder: (_ index: Int) -> [SequenceAnimatable] + ) { } + public static func through( _ sequence: S, - @AnimationBuilder animations builder: (S.Element) -> [SequenceAnimatable] + @SequenceBuilder animations builder: (S.Element) -> [SequenceAnimatable] ) -> [SequenceAnimatable] { - map(sequence, with: builder) + [] } -} -extension Loop: GroupConvertible where Looped == GroupAnimatable { - - public func asGroup() -> [GroupAnimatable] { - animations - } - - /// Creates a new Loop that repeats for the given amount of times. - /// - Parameters: - /// - repeatCount: How many times the loop should repeat. The index of each loop is provided as a argument in the `animations` closure - /// - animations: Add each animation from within this closure. Animations added to this loop should conform to ``GroupAnimatable`` - public init( - for repeatCount: Int, - @AnimationBuilder animations builder: (_ index: Int) -> [GroupAnimatable] - ) { - self.init(repeatCount: repeatCount, builder: builder) - } - - /// Loop through a sequence of values, like objects in an array or a range of numbers - /// - Parameters: - /// - sequence: Sequence to loop through, each element will be handled in the `animations` closure - /// - animations: Add each animation from within this closure. Animations added to this loop should conform to ``GroupAnimatable`` - /// - Returns: Group of all animations created in the `animation` closure public static func through( _ sequence: S, - @AnimationBuilder animations builder: (S.Element) -> [GroupAnimatable] - ) -> [GroupAnimatable] { - map(sequence, with: builder) - } -} - -extension Loop: PerformsAnimations where Looped == GroupAnimatable { - public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) { - Group(animations: animations).animate(delay: leadingDelay, completion: completion) - } -} - -extension Swift.Sequence { - /// Maps values from the sequence to animations - /// - Parameter animations: Add each animation from within this closure. Animations should conform to ``GroupAnimatable`` - /// - Returns: Sequence of all animations created in the `animation` closure - public func mapAnimations( - @AnimationBuilder animations builder: (Element) -> [SequenceAnimatable] - ) -> [SequenceAnimatable] { - flatMap(builder) - } - - /// Maps values from the sequence to animations - /// - Parameter animations: Add each animation from within this closure. Animations added to this loop should conform to ``GroupAnimatable`` - /// - Returns: Group of all animations created in the `animation` closure - public func mapAnimations( - @AnimationBuilder animations builder: (Element) -> [GroupAnimatable] + @SequenceBuilder animations builder: (S.Element) -> [GroupAnimatable] ) -> [GroupAnimatable] { - flatMap(builder) + [] } } diff --git a/Sources/AnimationPlanner/Animations/MapSequence.swift b/Sources/AnimationPlanner/Animations/MapSequence.swift new file mode 100644 index 0000000..58b3532 --- /dev/null +++ b/Sources/AnimationPlanner/Animations/MapSequence.swift @@ -0,0 +1,29 @@ +import Foundation + +extension Swift.Sequence { + @available(*, unavailable, renamed: "mapSequence", message: "use either mapSequence or mapGroup") + public func mapAnimations( + @SequenceBuilder animations builder: (Element) -> [SequenceAnimatable] + ) -> [SequenceAnimatable] { + flatMap(builder) + } + /// Maps values from the sequence to animations + /// - Parameter animations: Add each animation from within this closure. Animations should conform to ``GroupAnimatable`` + /// - Returns: Sequence of all animations created in the `animation` closure + public func mapSequence( + @SequenceBuilder animations builder: (Element) -> [SequenceAnimatable] + ) -> [SequenceAnimatable] { + flatMap(builder) + } +} + +extension Swift.Sequence { + /// Maps values from the sequence to animations + /// - Parameter animations: Add each animation from within this closure. Animations added to this loop should conform to ``GroupAnimatable`` + /// - Returns: Group of all animations created in the `animation` closure + public func mapGroup( + @GroupBuilder animations builder: (Element) -> [GroupAnimatable] + ) -> [GroupAnimatable] { + flatMap(builder) + } +} diff --git a/Sources/AnimationPlanner/Animations/Sequence.swift b/Sources/AnimationPlanner/Animations/Sequence.swift index e79ddbd..7973936 100644 --- a/Sources/AnimationPlanner/Animations/Sequence.swift +++ b/Sources/AnimationPlanner/Animations/Sequence.swift @@ -18,9 +18,9 @@ public struct Sequence: DelayedAnimatable { self.runningSequence = RunningSequence(animations: animations) } - /// Creates a new `Sequence` providing a way to perform a sequence animation from withing a group. Each animation is perform in in order, meaning each subsequent animation starts right after the previous completes. + /// Creates a new `Sequence` providing a way to perform a sequence animation from within a group. Each animation is perform in in order, meaning each subsequent animation starts right after the previous completes. /// - Parameter animations: Add each animation from within this closure. Animations added to a sequence should conform to ``SequenceAnimatable``. - public init(@AnimationBuilder animations builder: () -> [SequenceAnimatable]) { + public init(@SequenceBuilder animations builder: () -> [SequenceAnimatable]) { self.init(delay: 0, animations: builder()) } } diff --git a/Sources/AnimationPlanner/Documentation.docc/creating-basic-animation-sequence.md b/Sources/AnimationPlanner/Documentation.docc/creating-basic-animation-sequence.md index 476cfd3..cb0f318 100644 --- a/Sources/AnimationPlanner/Documentation.docc/creating-basic-animation-sequence.md +++ b/Sources/AnimationPlanner/Documentation.docc/creating-basic-animation-sequence.md @@ -1,6 +1,6 @@ # Creating a basic animation sequence -An example of how a typical animation sequence would look, how it‘s created and more information on the sample app available in the repository. +An example of how a typical animation sequence would look, how it’s created and more information on the sample app available in the repository. ## Overview diff --git a/Sources/AnimationPlanner/Extensions/CAMediaTimingFunction.swift b/Sources/AnimationPlanner/Extensions/CAMediaTimingFunction.swift index cf1486a..8d12f68 100644 --- a/Sources/AnimationPlanner/Extensions/CAMediaTimingFunction.swift +++ b/Sources/AnimationPlanner/Extensions/CAMediaTimingFunction.swift @@ -1,5 +1,5 @@ import QuartzCore -// from @warpling‘s https://gist.github.com/warpling/21bef9059e47f5aad2f2955d48fd7c0c +// from @warpling’s https://gist.github.com/warpling/21bef9059e47f5aad2f2955d48fd7c0c public extension CAMediaTimingFunction { static let linear = CAMediaTimingFunction(name: .linear) diff --git a/Sources/AnimationPlanner/Protocols/Animatable.swift b/Sources/AnimationPlanner/Protocols/Animatable.swift index 875335f..a4c08f1 100644 --- a/Sources/AnimationPlanner/Protocols/Animatable.swift +++ b/Sources/AnimationPlanner/Protocols/Animatable.swift @@ -20,14 +20,14 @@ public protocol Animation: Animatable, PerformsAnimations { public protocol SequenceAnimatable: Animatable, SequenceConvertible { } extension SequenceAnimatable { - public func asSequence() -> [SequenceAnimatable] { [self] } + public func animations() -> [SequenceAnimatable] { [self] } } /// Animation that can be used in a ``Group`` and be performed simultaneously, meaning all animations run at the same time. public protocol GroupAnimatable: Animatable, GroupConvertible { } extension GroupAnimatable { - public func asGroup() -> [GroupAnimatable] { [self] } + public func animations() -> [GroupAnimatable] { [self] } } /// 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. @@ -44,7 +44,7 @@ public protocol SpringAnimatable: Animatable { /// “To smoothly decelerate the animation without oscillation, use a value of 1. Employ a damping ratio closer to zero to increase oscillation.” var dampingRatio: CGFloat { get } - /// Initial velocity for spring-based animation. `UIView`‘s documentation clearly explains it with: + /// Initial velocity for spring-based animation. `UIView`’s documentation clearly explains it with: /// “A value of 1 corresponds to the total animation distance traversed in one second. For example, if the total animation distance is 200 points and you want the start of the animation to match a view velocity of 100 pt/s, use a value of 0.5.” var initialVelocity: CGFloat { get } } diff --git a/Sources/AnimationPlanner/Protocols/AnimationConvertible.swift b/Sources/AnimationPlanner/Protocols/AnimationConvertible.swift index 1f88f9d..34b1e3b 100644 --- a/Sources/AnimationPlanner/Protocols/AnimationConvertible.swift +++ b/Sources/AnimationPlanner/Protocols/AnimationConvertible.swift @@ -1,17 +1,27 @@ /// Provides a way to create a uniform sequence from all animations conforming to ``SequenceAnimatable`` public protocol SequenceConvertible { - func asSequence() -> [SequenceAnimatable] + func animations() -> [SequenceAnimatable] +} +extension SequenceConvertible where Self: SequenceAnimatable { + public func animations() -> [SequenceAnimatable] { + [self] + } } -/// Provides a way to group toghether animations conforming to ``GroupAnimatable`` +/// Provides a way to group together animations conforming to ``GroupAnimatable`` public protocol GroupConvertible { - func asGroup() -> [GroupAnimatable] + func animations() -> [GroupAnimatable] +} +extension GroupConvertible where Self: GroupAnimatable { + public func animations() -> [GroupAnimatable] { + [self] + } } extension Array: SequenceConvertible where Element == SequenceAnimatable { - public func asSequence() -> [SequenceAnimatable] { flatMap { $0.asSequence() } } + public func animations() -> [SequenceAnimatable] { flatMap { $0.animations() } } } extension Array: GroupConvertible where Element == GroupAnimatable { - public func asGroup() -> [GroupAnimatable] { flatMap { $0.asGroup() } } + public func animations() -> [GroupAnimatable] { flatMap { $0.animations() } } } diff --git a/Tests/AnimationPlannerTests/BuilderTests.swift b/Tests/AnimationPlannerTests/BuilderTests.swift index 581e2d6..2ceeba1 100644 --- a/Tests/AnimationPlannerTests/BuilderTests.swift +++ b/Tests/AnimationPlannerTests/BuilderTests.swift @@ -258,7 +258,7 @@ class BuilderTests: AnimationPlannerTests { AnimationPlanner.plan { Wait(delay) Group { - zip(views, animations).mapAnimations { view, animation in + zip(views, animations).mapGroup { view, animation in Sequence { Wait(animation.delay) Animate(duration: animation.duration) { diff --git a/Tests/AnimationPlannerTests/GroupTests.swift b/Tests/AnimationPlannerTests/GroupTests.swift index 9828232..504a841 100644 --- a/Tests/AnimationPlannerTests/GroupTests.swift +++ b/Tests/AnimationPlannerTests/GroupTests.swift @@ -10,17 +10,16 @@ class GroupTests: AnimationPlannerTests { let longestDuration = durations.max()! runAnimationBuilderTest(duration: longestDuration) { _, _ in - + AnimationPlanner.plan { Group { - durations.mapAnimations { duration in + durations.mapGroup { duration in Animate(duration: duration) { self.performRandomAnimation() } } } } - } } @@ -32,9 +31,9 @@ class GroupTests: AnimationPlannerTests { let longestDuration = durations.max()! runAnimationBuilderTest(duration: longestDuration) { _, _ in - + AnimationPlanner.group { - durations.mapAnimations { duration in + durations.mapGroup { duration in Animate(duration: duration) { self.performRandomAnimation() } @@ -121,7 +120,7 @@ class GroupTests: AnimationPlannerTests { AnimationPlanner.plan { Group { - animations.mapAnimations { animation in + animations.mapGroup { animation in AnimateDelayed(delay: animation.delay, duration: animation.duration) { self.performRandomAnimation() } @@ -144,5 +143,41 @@ class GroupTests: AnimationPlannerTests { } } + + func testGroupCountedLoop() { + let numberOfLoops: Int = 4 + let animations = randomDelayedAnimations(amount: numberOfLoops) + let totalDuration: TimeInterval = animations.max { $0.totalDuration < $1.totalDuration }?.totalDuration ?? 0 + + runAnimationBuilderTest(duration: totalDuration) { _, _ in + + AnimationPlanner.group { + for index in 0..