Skip to content

Commit

Permalink
Phase 3 - Remove old API (#15)
Browse files Browse the repository at this point in the history
* Much cleaner implementation of animation performing

* Reorganizing code structure

* Automatically add completion handler in builder tests

* Rename automatic builder test

* Remove old-style code 👋
  • Loading branch information
PimCoumans authored Jun 24, 2022
1 parent 67bfd64 commit 063641d
Show file tree
Hide file tree
Showing 22 changed files with 697 additions and 1,106 deletions.
304 changes: 46 additions & 258 deletions Sources/AnimationPlanner/AnimationPlanner.swift

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions Sources/AnimationPlanner/Animations/Animate.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
56 changes: 56 additions & 0 deletions Sources/AnimationPlanner/Animations/AnimateDelayed.swift
Original file line number Diff line number Diff line change
@@ -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<Delayed: Animatable>: 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)
}
}
56 changes: 56 additions & 0 deletions Sources/AnimationPlanner/Animations/AnimateSpring.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import UIKit

/// Performs an animation with spring dampening applied, using the same values as UIView spring animations
public struct AnimateSpring<Springed: Animation>: 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
)
}
}
44 changes: 44 additions & 0 deletions Sources/AnimationPlanner/Animations/AnimationBuilder.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading

0 comments on commit 063641d

Please sign in to comment.