Skip to content

Commit

Permalink
Much cleaner way of stopping in-flight animations
Browse files Browse the repository at this point in the history
  • Loading branch information
PimCoumans committed Apr 16, 2024
1 parent 003b57b commit a34558d
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 29 deletions.
17 changes: 14 additions & 3 deletions Sources/AnimationPlanner/Animations/Animate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public struct Animate: Animation, SequenceAnimatable, GroupAnimatable {
public internal(set) var options: UIView.AnimationOptions?
public internal(set) var timingFunction: CAMediaTimingFunction?

/// Class that holds stopped state
private let stopper: Stopper

/// 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``.
Expand All @@ -25,12 +28,21 @@ public struct Animate: Animation, SequenceAnimatable, GroupAnimatable {
timingFunction: CAMediaTimingFunction? = nil,
changes: @escaping () -> Void = {}
) {
let stopper = Stopper()
self.duration = duration
self.timingFunction = timingFunction
self.changes = changes
self.changes = { [weak stopper] in
guard stopper?.isStopped == false else { return }
changes()
}
self.stopper = stopper
}
}

internal class Stopper {
var isStopped: Bool = false
}

extension Animate: PerformsAnimations {
public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) {
let timing = timingParameters(leadingDelay: leadingDelay)
Expand Down Expand Up @@ -58,7 +70,6 @@ extension Animate: PerformsAnimations {
}

public func stop() {
/// Just run animation again but ridiculously short and from current state
UIView.animate(withDuration: 0.001, delay: 0, options: .beginFromCurrentState, animations: changes)
stopper.isStopped = true
}
}
35 changes: 14 additions & 21 deletions Sources/AnimationPlanner/Animations/Extra.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,35 @@ import UIKit
/// 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 {
private let id = UUID()
public let duration: TimeInterval = 0

public var perform: () -> Void
/// Work item used for actually executing the closure
private let workItem: DispatchWorkItem

public init(perform: @escaping () -> Void) {
self.perform = perform
workItem = DispatchWorkItem(block: perform)
}
}

extension Extra {
private static var allowedAnimations: Set<UUID> = []
}

extension Extra: PerformsAnimations {
public func animate(delay leadingDelay: TimeInterval, completion: ((Bool) -> Void)?) {
let timing = timingParameters(leadingDelay: leadingDelay)
Self.allowedAnimations.insert(id)
let animation: () -> Void = {
guard Self.allowedAnimations.contains(id) else {
completion?(false)
return
}
self.perform()
Self.allowedAnimations.remove(id)
completion?(true)
}

guard timing.delay > 0 else {
animation()
workItem.perform()
completion?(true)
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + timing.delay) {
animation()

workItem.notify(queue: .main) { [weak workItem] in
let isFinished = workItem?.isCancelled != false
completion?(isFinished)
}

DispatchQueue.main.asyncAfter(deadline: .now() + timing.delay, execute: workItem)
}

public func stop() {
Self.allowedAnimations.remove(id)
workItem.cancel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ extension Mutable {
}

extension Animate: Mutable { }
extension Extra: Mutable { }

extension AnimateSpring: Mutable { }
extension AnimateSpring: AnimationModifiers where Contained: AnimationModifiers {
Expand Down
8 changes: 3 additions & 5 deletions Tests/AnimationPlannerTests/StoppingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ class StoppingTests: AnimationPlannerTests {
}

func testGroupStopping() {
// Runs a number of sequences in parallel with each a specific number of animations.
// Runs a number of sequences in parallel with each a specific number of animations
// Then halfway through the expected duration of the animations, the animations are
// stopped. Each Extra that is executed after animations that should be stopped triggers
// stopped. Each animation that is executed after animations should be stopped triggers
// a failure.

let numberOfSequences = 4
Expand All @@ -110,10 +110,8 @@ class StoppingTests: AnimationPlannerTests {
for animation in animations {
Wait(animation.delay)
Animate(duration: animation.duration) {
self.performRandomAnimation(on: view)
}
Extra {
XCTAssert(CACurrentMediaTime() < (pauseOffset + durationPrecision))
self.performRandomAnimation(on: view)
}
}
}
Expand Down

0 comments on commit a34558d

Please sign in to comment.