From 4fa928d8c07c14adca2d7e3f1395b18a80385c2c Mon Sep 17 00:00:00 2001 From: Nick Entin Date: Sun, 25 Oct 2020 19:40:23 -0700 Subject: [PATCH] Move source of truth for duration and repeat style to the execution phase (#49) * Adds `duration` and `repeatStyle` parameters to the `Animation.perform(...)` and `AnimationQueue.enqueue(...)` methods to allow for specifying an explicit duration and repeat style at the start of the execution phase. * Renames the `duration` properties on `Animation` and `AnimationGroup` to `implicitDuration`. * Renames the `repeatStyle` properties on `Animation` and `AnimationGroup` to `implicitRepeatStyle`. * Renames `AnimationRepeatStyle.none` to `.noRepeat` to avoid an ambiguous reference when using an optional repeat style. * Updates a lot of headerdocs to better explain how the duration and repeat styles are resolved. --- .../Contents.swift | 3 +- .../Contents.swift | 2 +- .../Contents.swift | 4 +- .../Contents.swift | 2 +- .../Contents.swift | 6 +- .../Contents.swift | 2 +- .../Contents.swift | 8 +-- .../AnimationCancelationViewController.swift | 3 +- .../AnimationCurveViewController.swift | 2 +- .../AnimationGroupViewController.swift | 4 +- .../AnimationQueueViewController.swift | 2 +- ...ChildAnimationProgressViewController.swift | 2 +- .../ChildAnimationsViewController.swift | 4 +- ...ldAnimationsWithCurvesViewController.swift | 4 +- .../ColorAnimationsViewController.swift | 18 ++---- .../ExecutionBlockViewController.swift | 8 ++- .../PerformanceBenchmarkViewController.swift | 8 +-- .../PropertyAssignmentViewController.swift | 7 +-- .../RepeatingAnimationsViewController.swift | 55 +++++++++++-------- Example/Unit Tests/AnimationGroupTests.swift | 20 +++---- .../Unit Tests/AnimationSnapshotTests.swift | 6 +- ...ransform3DInterpolationSnapshotTests.swift | 2 +- ...eTransformInterpolationSnapshotTests.swift | 2 +- .../Unit Tests/DisplayLinkDriverTests.swift | 16 +++--- Sources/Stagehand/Animation/Animation.swift | 43 ++++++++++----- Sources/Stagehand/AnimationGroup.swift | 35 +++++++++--- .../AnimationInstance/AnimationInstance.swift | 5 +- Sources/Stagehand/AnimationQueue.swift | 23 +++++++- .../FBSnapshotTestCase+AnimationGIF.swift | 12 ++-- 29 files changed, 179 insertions(+), 129 deletions(-) diff --git a/Example/Stagehand Tutorial.playground/Pages/Animation Groups.xcplaygroundpage/Contents.swift b/Example/Stagehand Tutorial.playground/Pages/Animation Groups.xcplaygroundpage/Contents.swift index 77b11e5..47f1f1b 100644 --- a/Example/Stagehand Tutorial.playground/Pages/Animation Groups.xcplaygroundpage/Contents.swift +++ b/Example/Stagehand Tutorial.playground/Pages/Animation Groups.xcplaygroundpage/Contents.swift @@ -24,7 +24,8 @@ animationGroup.addAnimation(secondAnimation, for: secondElement, startingAt: 0, /*: - Like normal `Animation`s, we can change the `duration`, `curve`, and `repeatStyle` of our animation group as a whole. + Like normal `Animation`s, we can change the `implicitDuration`, `curve`, and `implicitRepeatStyle` of our animation + group as a whole. When we're ready to perform the animation, we call the `perform(delay:completion:)` method. diff --git a/Example/Stagehand Tutorial.playground/Pages/Assigning Properties During Animations.xcplaygroundpage/Contents.swift b/Example/Stagehand Tutorial.playground/Pages/Assigning Properties During Animations.xcplaygroundpage/Contents.swift index 7ec1bc3..d5f8df5 100644 --- a/Example/Stagehand Tutorial.playground/Pages/Assigning Properties During Animations.xcplaygroundpage/Contents.swift +++ b/Example/Stagehand Tutorial.playground/Pages/Assigning Properties During Animations.xcplaygroundpage/Contents.swift @@ -42,7 +42,7 @@ propertyAssignmentAnimation.addAssignment(for: \.clipsToBounds, at: 0.5, value: */ -propertyAssignmentAnimation.repeatStyle = .infinitlyRepeating(autoreversing: true) +propertyAssignmentAnimation.implicitRepeatStyle = .infinitelyRepeating(autoreversing: true) let view = ExpandedBoundsView(frame: .init(x: 0, y: 0, width: 100, height: 100)) PlaygroundPage.current.liveView = WrapperView(wrappedView: view) diff --git a/Example/Stagehand Tutorial.playground/Pages/Composing Animations.xcplaygroundpage/Contents.swift b/Example/Stagehand Tutorial.playground/Pages/Composing Animations.xcplaygroundpage/Contents.swift index 699888c..62c1b1d 100644 --- a/Example/Stagehand Tutorial.playground/Pages/Composing Animations.xcplaygroundpage/Contents.swift +++ b/Example/Stagehand Tutorial.playground/Pages/Composing Animations.xcplaygroundpage/Contents.swift @@ -39,7 +39,7 @@ func makeFlatAnimation() -> Animation { animation.addKeyframe(for: \.bottomView.backgroundColor, at: 0.75, value: UIColor.yellow.withAlphaComponent(0.8)) animation.addKeyframe(for: \.bottomView.backgroundColor, at: 1, value: .yellow) - animation.duration = 3 + animation.implicitDuration = 3 return animation } @@ -94,7 +94,7 @@ func makeHierarchicalAnimation() -> Animation { animation.addChild(makeCarAnimation(), for: \.topView, startingAt: 0, relativeDuration: 1) animation.addChild(makeCarAnimation(), for: \.bottomView, startingAt: 0, relativeDuration: 1) - animation.duration = 3 + animation.implicitDuration = 3 return animation } diff --git a/Example/Stagehand Tutorial.playground/Pages/Creating and Executing an Animation.xcplaygroundpage/Contents.swift b/Example/Stagehand Tutorial.playground/Pages/Creating and Executing an Animation.xcplaygroundpage/Contents.swift index b490230..4b90039 100644 --- a/Example/Stagehand Tutorial.playground/Pages/Creating and Executing an Animation.xcplaygroundpage/Contents.swift +++ b/Example/Stagehand Tutorial.playground/Pages/Creating and Executing an Animation.xcplaygroundpage/Contents.swift @@ -18,7 +18,7 @@ import Stagehand var basicAnimation = Animation() -basicAnimation.duration = 2 +basicAnimation.implicitDuration = 2 /*: diff --git a/Example/Stagehand Tutorial.playground/Pages/Executing Code During Animations.xcplaygroundpage/Contents.swift b/Example/Stagehand Tutorial.playground/Pages/Executing Code During Animations.xcplaygroundpage/Contents.swift index d4c10ae..18ce6d8 100644 --- a/Example/Stagehand Tutorial.playground/Pages/Executing Code During Animations.xcplaygroundpage/Contents.swift +++ b/Example/Stagehand Tutorial.playground/Pages/Executing Code During Animations.xcplaygroundpage/Contents.swift @@ -95,7 +95,7 @@ func makeAnimation(model: ModelDrivenView.Model) -> Animation { animation.addKeyframe(for: \.alpha, at: 1, value: 1) // Make the total duration of the animation 2 seconds. - animation.duration = 2 + animation.implicitDuration = 2 return animation } @@ -148,8 +148,8 @@ func makeAnimation(modelToFlash: ModelDrivenView.Model, modelToRestore: ModelDri at: 0.5 ) - animation.duration = 2 - animation.repeatStyle = .repeating(count: 2, autoreversing: true) + animation.implicitDuration = 2 + animation.implicitRepeatStyle = .repeating(count: 2, autoreversing: true) return animation } diff --git a/Example/Stagehand Tutorial.playground/Pages/Executing Code Every Frame.xcplaygroundpage/Contents.swift b/Example/Stagehand Tutorial.playground/Pages/Executing Code Every Frame.xcplaygroundpage/Contents.swift index 7373ec7..032b2d9 100644 --- a/Example/Stagehand Tutorial.playground/Pages/Executing Code Every Frame.xcplaygroundpage/Contents.swift +++ b/Example/Stagehand Tutorial.playground/Pages/Executing Code Every Frame.xcplaygroundpage/Contents.swift @@ -31,7 +31,7 @@ final class DisplayLinkAnimator { // MARK: - Private Properties - private let displayLink: CADisplayLink! + private var displayLink: CADisplayLink! private let startTime: CFTimeInterval diff --git a/Example/Stagehand Tutorial.playground/Pages/Repeating Animations.xcplaygroundpage/Contents.swift b/Example/Stagehand Tutorial.playground/Pages/Repeating Animations.xcplaygroundpage/Contents.swift index 91a531a..5504962 100644 --- a/Example/Stagehand Tutorial.playground/Pages/Repeating Animations.xcplaygroundpage/Contents.swift +++ b/Example/Stagehand Tutorial.playground/Pages/Repeating Animations.xcplaygroundpage/Contents.swift @@ -9,14 +9,14 @@ import Stagehand By default, animations will run once before completing. Sometimes, though, we want our animation to loop through multiple times, sometimes even indefinitely. - Using the `Animation.repeatStyle` property, we can control how our animation repeats. + Using the `Animation.implicitRepeatStyle` property, we can control how our animation repeats. */ var animation = Animation() // The default style is to not repeat. -animation.repeatStyle = .none +animation.implicitRepeatStyle = .noRepeat /*: @@ -25,7 +25,7 @@ animation.repeatStyle = .none */ -animation.repeatStyle = .repeating(count: 2, autoreversing: false) +animation.implicitRepeatStyle = .repeating(count: 2, autoreversing: false) /*: @@ -34,7 +34,7 @@ animation.repeatStyle = .repeating(count: 2, autoreversing: false) */ -animation.repeatStyle = .infinitelyRepeating(autoreversing: false) +animation.implicitRepeatStyle = .infinitelyRepeating(autoreversing: false) /*: diff --git a/Example/Stagehand/AnimationCancelationViewController.swift b/Example/Stagehand/AnimationCancelationViewController.swift index 9a0283f..0348e9d 100644 --- a/Example/Stagehand/AnimationCancelationViewController.swift +++ b/Example/Stagehand/AnimationCancelationViewController.swift @@ -32,7 +32,7 @@ final class AnimationCancelationViewController: DemoViewController { self.animationInstance?.cancel() let animation = self.makeAnimation() - self.animationInstance = animation.perform(on: self.mainView.animatableView) + self.animationInstance = animation.perform(on: self.mainView.animatableView, duration: 2) }), ("Cancel (Revert)", { [unowned self] in self.animationInstance?.cancel(behavior: .revert) @@ -58,7 +58,6 @@ final class AnimationCancelationViewController: DemoViewController { var animation = Animation() animation.addKeyframe(for: \.transform, at: 0, value: .identity) animation.addKeyframe(for: \.transform, at: 1, value: .init(translationX: mainView.bounds.width - 100, y: 0)) - animation.duration = 2 return animation } diff --git a/Example/Stagehand/AnimationCurveViewController.swift b/Example/Stagehand/AnimationCurveViewController.swift index 9514577..c63713e 100644 --- a/Example/Stagehand/AnimationCurveViewController.swift +++ b/Example/Stagehand/AnimationCurveViewController.swift @@ -117,7 +117,7 @@ final class AnimationCurveViewController: DemoViewController { var animation = Animation() animation.addKeyframe(for: \.transform, at: 0, value: .identity) animation.addKeyframe(for: \.transform, at: 1, value: .init(translationX: mainView.bounds.width - 100, y: 0)) - animation.duration = 2 + animation.implicitDuration = 2 animation.addPerFrameExecution { [weak self] context in guard let self = self else { return } diff --git a/Example/Stagehand/AnimationGroupViewController.swift b/Example/Stagehand/AnimationGroupViewController.swift index 5f2dbe6..8db9294 100644 --- a/Example/Stagehand/AnimationGroupViewController.swift +++ b/Example/Stagehand/AnimationGroupViewController.swift @@ -29,7 +29,6 @@ final class AnimationGroupViewController: DemoViewController { animationRows = [ ("Move Both Views", { [unowned self] in var animationGroup = AnimationGroup() - animationGroup.duration = 2 let topAnimation = self.makeAnimation() animationGroup.addAnimation(topAnimation, for: self.topView, startingAt: 0, relativeDuration: 0.75) @@ -37,7 +36,7 @@ final class AnimationGroupViewController: DemoViewController { let bottomAnimation = self.makeAnimation() animationGroup.addAnimation(bottomAnimation, for: self.bottomView, startingAt: 0.25, relativeDuration: 0.75) - animationGroup.perform() + animationGroup.perform(duration: 2) }), ] } @@ -54,7 +53,6 @@ final class AnimationGroupViewController: DemoViewController { var animation = Animation() animation.addKeyframe(for: \.transform, at: 0, value: .identity) animation.addKeyframe(for: \.transform, at: 1, value: .init(translationX: contentView.bounds.width - 100, y: 0)) - animation.duration = 2 return animation } diff --git a/Example/Stagehand/AnimationQueueViewController.swift b/Example/Stagehand/AnimationQueueViewController.swift index c9e6b63..950847f 100644 --- a/Example/Stagehand/AnimationQueueViewController.swift +++ b/Example/Stagehand/AnimationQueueViewController.swift @@ -66,7 +66,7 @@ final class AnimationQueueViewController: DemoViewController { private func makeTranslationAnimation(x: CGFloat, y: CGFloat) -> Animation { var animation = Animation() - animation.duration = 2 + animation.implicitDuration = 2 animation.addKeyframe(for: \.animatableView.transform, at: 0, relativeValue: { $0 }) animation.addKeyframe(for: \.animatableView.transform, at: 1, value: .init(translationX: x, y: y)) diff --git a/Example/Stagehand/ChildAnimationProgressViewController.swift b/Example/Stagehand/ChildAnimationProgressViewController.swift index 1caf921..6dd2312 100644 --- a/Example/Stagehand/ChildAnimationProgressViewController.swift +++ b/Example/Stagehand/ChildAnimationProgressViewController.swift @@ -246,7 +246,7 @@ final class ChildAnimationProgressViewController: DemoViewController { parentAnimation.addChild(childAnimation, for: \.self, startingAt: 0.25, relativeDuration: 0.5) - parentAnimation.duration = 4 + parentAnimation.implicitDuration = 4 return parentAnimation } diff --git a/Example/Stagehand/ChildAnimationsViewController.swift b/Example/Stagehand/ChildAnimationsViewController.swift index 9a860e9..5ad1225 100644 --- a/Example/Stagehand/ChildAnimationsViewController.swift +++ b/Example/Stagehand/ChildAnimationsViewController.swift @@ -119,9 +119,7 @@ final class ChildAnimationsViewController: DemoViewController { animation.addChild(fadeOutAnimation, for: \.self, startingAt: 0, relativeDuration: 0.5) animation.addChild(fadeInAnimation, for: \.self, startingAt: 0.5, relativeDuration: 0.5) - animation.duration = 2 - - animation.perform(on: self.mainView) + animation.perform(on: self.mainView, duration: 2) }), ] } diff --git a/Example/Stagehand/ChildAnimationsWithCurvesViewController.swift b/Example/Stagehand/ChildAnimationsWithCurvesViewController.swift index 6f98551..206d347 100644 --- a/Example/Stagehand/ChildAnimationsWithCurvesViewController.swift +++ b/Example/Stagehand/ChildAnimationsWithCurvesViewController.swift @@ -33,7 +33,6 @@ final class ChildAnimationsWithCurvesViewController: DemoViewController { }), ("Linear / Ease In Ease Out", { [unowned self] in var animation = Animation() - animation.duration = 2 var topAnimation = self.makeAnimation() topAnimation.curve = LinearAnimationCurve() @@ -43,7 +42,7 @@ final class ChildAnimationsWithCurvesViewController: DemoViewController { bottomAnimation.curve = SinusoidalEaseInEaseOutAnimationCurve() animation.addChild(bottomAnimation, for: \View.bottomView, startingAt: 0, relativeDuration: 1) - animation.perform(on: self.mainView) + animation.perform(on: self.mainView, duration: 2) }), ] } @@ -58,7 +57,6 @@ final class ChildAnimationsWithCurvesViewController: DemoViewController { var animation = Animation() animation.addKeyframe(for: \.transform, at: 0, value: .identity) animation.addKeyframe(for: \.transform, at: 1, value: .init(translationX: mainView.bounds.width - 100, y: 0)) - animation.duration = 2 return animation } diff --git a/Example/Stagehand/ColorAnimationsViewController.swift b/Example/Stagehand/ColorAnimationsViewController.swift index 9a1e3a2..449b669 100644 --- a/Example/Stagehand/ColorAnimationsViewController.swift +++ b/Example/Stagehand/ColorAnimationsViewController.swift @@ -40,69 +40,63 @@ final class ColorAnimationsViewController: DemoViewController { self.animationInstance?.cancel() var animation = Animation() - animation.duration = 2 animation.addKeyframe(for: \.backgroundColor, at: 0, value: .red) animation.addKeyframe(for: \.backgroundColor, at: 1, value: .green) - self.animationInstance = animation.perform(on: self.contentView) + self.animationInstance = animation.perform(on: self.contentView, duration: 2) }), ("Red (sRGB) -> nil -> Green (sRGB)", { [unowned self] in self.animationInstance?.cancel() var animation = Animation() - animation.duration = 2 animation.addKeyframe(for: \.backgroundColor, at: 0, value: .red) animation.addKeyframe(for: \.backgroundColor, at: 0.5, value: nil) animation.addKeyframe(for: \.backgroundColor, at: 1, value: .green) - self.animationInstance = animation.perform(on: self.contentView) + self.animationInstance = animation.perform(on: self.contentView, duration: 2) }), ("Red (P3) -> Green (P3)", { [unowned self] in self.animationInstance?.cancel() var animation = Animation() - animation.duration = 2 animation.addKeyframe(for: \UIView.backgroundColor, at: 0, value: UIColor(displayP3Red: 1, green: 0, blue: 0, alpha: 1)) animation.addKeyframe(for: \UIView.backgroundColor, at: 1, value: UIColor(displayP3Red: 0, green: 1, blue: 0, alpha: 1)) - self.animationInstance = animation.perform(on: self.contentView) + self.animationInstance = animation.perform(on: self.contentView, duration: 2) }), ("Red (sRGB) -> Green (P3)", { [unowned self] in self.animationInstance?.cancel() var animation = Animation() - animation.duration = 2 animation.addKeyframe(for: \UIView.backgroundColor, at: 0, value: .red) animation.addKeyframe(for: \UIView.backgroundColor, at: 1, value: UIColor(displayP3Red: 0, green: 1, blue: 0, alpha: 1)) - self.animationInstance = animation.perform(on: self.contentView) + self.animationInstance = animation.perform(on: self.contentView, duration: 2) }), ("Red (sRGB) -> Red (P3)", { [unowned self] in self.animationInstance?.cancel() var animation = Animation() - animation.duration = 2 animation.addKeyframe(for: \UIView.backgroundColor, at: 0, value: .red) animation.addKeyframe(for: \UIView.backgroundColor, at: 1, value: UIColor(displayP3Red: 1, green: 0, blue: 0, alpha: 1)) - self.animationInstance = animation.perform(on: self.contentView) + self.animationInstance = animation.perform(on: self.contentView, duration: 2) }), ("Red (sRGB), with alpha 1 -> 0.5 -> 1", { [unowned self] in self.animationInstance?.cancel() var animation = Animation() - animation.duration = 2 animation.addKeyframe(for: \UIView.backgroundColor, at: 0.0, value: UIColor.red) animation.addKeyframe(for: \UIView.backgroundColor, at: 0.5, value: UIColor.red.withAlphaComponent(0.5)) animation.addKeyframe(for: \UIView.backgroundColor, at: 1.0, value: UIColor.red) - self.animationInstance = animation.perform(on: self.contentView) + self.animationInstance = animation.perform(on: self.contentView, duration: 2) }), ] } diff --git a/Example/Stagehand/ExecutionBlockViewController.swift b/Example/Stagehand/ExecutionBlockViewController.swift index 9fb5488..0de64ce 100644 --- a/Example/Stagehand/ExecutionBlockViewController.swift +++ b/Example/Stagehand/ExecutionBlockViewController.swift @@ -66,10 +66,13 @@ final class ExecutionBlockViewController: DemoViewController { }, at: 1 ) - animation.repeatStyle = .repeating(count: 2, autoreversing: true) feedbackGenerator.prepare() - self.animationInstance = animation.perform(on: self.mainView.animatableView) + self.animationInstance = animation.perform( + on: self.mainView.animatableView, + duration: 2, + repeatStyle: .repeating(count: 2, autoreversing: true) + ) }), ] } @@ -86,7 +89,6 @@ final class ExecutionBlockViewController: DemoViewController { var animation = Animation() animation.addKeyframe(for: \.transform, at: 0, value: .identity) animation.addKeyframe(for: \.transform, at: 1, value: .init(translationX: mainView.bounds.width - 100, y: 0)) - animation.duration = 2 return animation } diff --git a/Example/Stagehand/PerformanceBenchmarkViewController.swift b/Example/Stagehand/PerformanceBenchmarkViewController.swift index 14b770f..fc2d9c0 100644 --- a/Example/Stagehand/PerformanceBenchmarkViewController.swift +++ b/Example/Stagehand/PerformanceBenchmarkViewController.swift @@ -41,8 +41,8 @@ final class PerformanceBenchmarkViewController: DemoViewController { animation.addKeyframe(for: \.centerView.transform, at: 0.75, value: .init(rotationAngle: .pi * 3 / 2)) animation.addKeyframe(for: \.centerView.transform, at: 1.00, value: .identity) - animation.duration = 4 - animation.repeatStyle = .infinitelyRepeating(autoreversing: true) + animation.implicitDuration = 4 + animation.implicitRepeatStyle = .infinitelyRepeating(autoreversing: true) self.animationInstances.append(animation.perform(on: self.mainView)) }), @@ -68,8 +68,8 @@ final class PerformanceBenchmarkViewController: DemoViewController { ) } - animation.duration = 3.5 - animation.repeatStyle = .infinitelyRepeating(autoreversing: false) + animation.implicitDuration = 3.5 + animation.implicitRepeatStyle = .infinitelyRepeating(autoreversing: false) self.animationInstances.append(animation.perform(on: self.mainView)) }), diff --git a/Example/Stagehand/PropertyAssignmentViewController.swift b/Example/Stagehand/PropertyAssignmentViewController.swift index e6e05f8..5140bc5 100644 --- a/Example/Stagehand/PropertyAssignmentViewController.swift +++ b/Example/Stagehand/PropertyAssignmentViewController.swift @@ -50,19 +50,18 @@ final class PropertyAssignmentViewController: DemoViewController { var animation = Animation() animation.addChild(childAnimation, for: \.animatableView, startingAt: 0, relativeDuration: 1) - animation.duration = 2 - self.animationInstance = animation.perform(on: self.mainView) + self.animationInstance = animation.perform(on: self.mainView, duration: 2) }), ("Current -> Yellow -> Green, with reversal", { [unowned self] in var animation = self.makeAnimation() animation.addAssignment(for: \.backgroundColor, at: 0.33, value: .yellow) animation.addAssignment(for: \.backgroundColor, at: 0.66, value: .green) - animation.repeatStyle = .repeating(count: 2, autoreversing: true) self.mainView.initialColorSlider.isEnabled = false self.animationInstance = animation.perform( on: self.mainView.animatableView, + repeatStyle: .repeating(count: 2, autoreversing: true), completion: { [weak self] _ in self?.mainView.initialColorSlider.isEnabled = true } @@ -85,7 +84,7 @@ final class PropertyAssignmentViewController: DemoViewController { var animation = Animation() animation.addKeyframe(for: \.transform, at: 0, value: .identity) animation.addKeyframe(for: \.transform, at: 1, value: .init(translationX: mainView.bounds.width - 100, y: 0)) - animation.duration = 2 + animation.implicitDuration = 2 return animation } diff --git a/Example/Stagehand/RepeatingAnimationsViewController.swift b/Example/Stagehand/RepeatingAnimationsViewController.swift index 133df8c..7fb9eac 100644 --- a/Example/Stagehand/RepeatingAnimationsViewController.swift +++ b/Example/Stagehand/RepeatingAnimationsViewController.swift @@ -31,57 +31,68 @@ final class RepeatingAnimationsViewController: DemoViewController { // Cancel any existing animation self.animationInstance?.cancel(behavior: .revert) - var animation = self.makeAnimation() - animation.repeatStyle = .none - self.animationInstance = animation.perform(on: self.mainView.animatableView) + let animation = self.makeAnimation() + self.animationInstance = animation.perform(on: self.mainView.animatableView, repeatStyle: .noRepeat) }), ("Repeat Once", { [unowned self] in // Cancel any existing animation self.animationInstance?.cancel(behavior: .revert) - var animation = self.makeAnimation() - animation.repeatStyle = .repeating(count: 2, autoreversing: false) - self.animationInstance = animation.perform(on: self.mainView.animatableView) + let animation = self.makeAnimation() + self.animationInstance = animation.perform( + on: self.mainView.animatableView, + repeatStyle: .repeating(count: 2, autoreversing: false) + ) }), ("Repeat Once, Autoreversing", { [unowned self] in // Cancel any existing animation self.animationInstance?.cancel(behavior: .revert) - var animation = self.makeAnimation() - animation.repeatStyle = .repeating(count: 2, autoreversing: true) - self.animationInstance = animation.perform(on: self.mainView.animatableView) + let animation = self.makeAnimation() + self.animationInstance = animation.perform( + on: self.mainView.animatableView, + repeatStyle: .repeating(count: 2, autoreversing: true) + ) }), ("Repeat Twice", { [unowned self] in // Cancel any existing animation self.animationInstance?.cancel(behavior: .revert) - var animation = self.makeAnimation() - animation.repeatStyle = .repeating(count: 3, autoreversing: false) - self.animationInstance = animation.perform(on: self.mainView.animatableView) + let animation = self.makeAnimation() + self.animationInstance = animation.perform( + on: self.mainView.animatableView, + repeatStyle: .repeating(count: 3, autoreversing: false) + ) }), ("Repeat Twice, Autoreversing", { [unowned self] in // Cancel any existing animation self.animationInstance?.cancel(behavior: .revert) - var animation = self.makeAnimation() - animation.repeatStyle = .repeating(count: 3, autoreversing: true) - self.animationInstance = animation.perform(on: self.mainView.animatableView) + let animation = self.makeAnimation() + self.animationInstance = animation.perform( + on: self.mainView.animatableView, + repeatStyle: .repeating(count: 3, autoreversing: true) + ) }), ("Repeat Infinitely", { [unowned self] in // Cancel any existing animation self.animationInstance?.cancel(behavior: .revert) - var animation = self.makeAnimation() - animation.repeatStyle = .infinitelyRepeating(autoreversing: false) - self.animationInstance = animation.perform(on: self.mainView.animatableView) + let animation = self.makeAnimation() + self.animationInstance = animation.perform( + on: self.mainView.animatableView, + repeatStyle: .infinitelyRepeating(autoreversing: false) + ) }), ("Repeat Infinitely, Autoreversing", { [unowned self] in // Cancel any existing animation self.animationInstance?.cancel(behavior: .revert) - var animation = self.makeAnimation() - animation.repeatStyle = .infinitelyRepeating(autoreversing: true) - self.animationInstance = animation.perform(on: self.mainView.animatableView) + let animation = self.makeAnimation() + self.animationInstance = animation.perform( + on: self.mainView.animatableView, + repeatStyle: .infinitelyRepeating(autoreversing: true) + ) }), ("Cancel (Revert)", { [unowned self] in self.animationInstance?.cancel(behavior: .revert) @@ -106,7 +117,7 @@ final class RepeatingAnimationsViewController: DemoViewController { var animation = Animation() animation.addKeyframe(for: \.transform, at: 0, value: .identity) animation.addKeyframe(for: \.transform, at: 1, value: .init(translationX: mainView.bounds.width - 100, y: 0)) - animation.duration = 1 + animation.implicitDuration = 1 return animation } diff --git a/Example/Unit Tests/AnimationGroupTests.swift b/Example/Unit Tests/AnimationGroupTests.swift index 4f229b9..ea91965 100644 --- a/Example/Unit Tests/AnimationGroupTests.swift +++ b/Example/Unit Tests/AnimationGroupTests.swift @@ -160,7 +160,7 @@ final class AnimationGroupTests: XCTestCase { func testCompletionCalledOnComplete() { var animationGroup = AnimationGroup() - animationGroup.duration = 0.05 + animationGroup.implicitDuration = 0.05 let completionExpectation = expectation(description: "Calls completion handler") animationGroup.addCompletionHandler { finished in @@ -175,7 +175,7 @@ final class AnimationGroupTests: XCTestCase { func testCompletionCalledOnCancel() { var animationGroup = AnimationGroup() - animationGroup.duration = 1 + animationGroup.implicitDuration = 1 let completionExpectation = expectation(description: "Calls completion handler") animationGroup.addCompletionHandler { finished in @@ -195,22 +195,22 @@ final class AnimationGroupTests: XCTestCase { var animationGroup = AnimationGroup() // The duration should default to 1 second. - XCTAssertEqual(animationGroup.duration, 1) + XCTAssertEqual(animationGroup.implicitDuration, 1) - animationGroup.duration = 3 - XCTAssertEqual(animationGroup.duration, 3) - XCTAssertEqual(animationGroup.animation.duration, 3) + animationGroup.implicitDuration = 3 + XCTAssertEqual(animationGroup.implicitDuration, 3) + XCTAssertEqual(animationGroup.animation.implicitDuration, 3) } func testRepeatStyle() { var animationGroup = AnimationGroup() // The repeat style should default to not repeating. - XCTAssertEqual(animationGroup.repeatStyle, .none) + XCTAssertEqual(animationGroup.implicitRepeatStyle, .noRepeat) - animationGroup.repeatStyle = .infinitelyRepeating(autoreversing: true) - XCTAssertEqual(animationGroup.repeatStyle, .infinitelyRepeating(autoreversing: true)) - XCTAssertEqual(animationGroup.animation.repeatStyle, .infinitelyRepeating(autoreversing: true)) + animationGroup.implicitRepeatStyle = .infinitelyRepeating(autoreversing: true) + XCTAssertEqual(animationGroup.implicitRepeatStyle, .infinitelyRepeating(autoreversing: true)) + XCTAssertEqual(animationGroup.animation.implicitRepeatStyle, .infinitelyRepeating(autoreversing: true)) } func testCurve() { diff --git a/Example/Unit Tests/AnimationSnapshotTests.swift b/Example/Unit Tests/AnimationSnapshotTests.swift index 40a6c39..b6998a7 100644 --- a/Example/Unit Tests/AnimationSnapshotTests.swift +++ b/Example/Unit Tests/AnimationSnapshotTests.swift @@ -104,7 +104,7 @@ final class AnimationSnapshotTests: SnapshotTestCase { var animation = Animation() animation.addKeyframe(for: \.animatableView.transform, at: 0, value: .identity) animation.addKeyframe(for: \.animatableView.transform, at: 1, value: .init(translationX: 160, y: 0)) - animation.duration = 2 + animation.implicitDuration = 2 SnapshotVerify(animation: animation, on: view) } @@ -115,7 +115,7 @@ final class AnimationSnapshotTests: SnapshotTestCase { var animation = Animation() animation.addKeyframe(for: \.animatableView.transform, at: 0, value: .identity) animation.addKeyframe(for: \.animatableView.transform, at: 1, value: .init(translationX: 160, y: 0)) - animation.repeatStyle = .infinitelyRepeating(autoreversing: true) + animation.implicitRepeatStyle = .infinitelyRepeating(autoreversing: true) SnapshotVerify(animation: animation, on: view, bookendFrameDuration: .matchIntermediateFrames) } @@ -146,7 +146,7 @@ final class AnimationSnapshotTests: SnapshotTestCase { onReverse: { $0.animatableView.backgroundColor = .red }, at: 0.5 ) - animation.repeatStyle = .infinitelyRepeating(autoreversing: true) + animation.implicitRepeatStyle = .infinitelyRepeating(autoreversing: true) SnapshotVerify(animation: animation, on: view) } diff --git a/Example/Unit Tests/CATransform3DInterpolationSnapshotTests.swift b/Example/Unit Tests/CATransform3DInterpolationSnapshotTests.swift index 7dea83e..bcb8ba3 100644 --- a/Example/Unit Tests/CATransform3DInterpolationSnapshotTests.swift +++ b/Example/Unit Tests/CATransform3DInterpolationSnapshotTests.swift @@ -149,7 +149,7 @@ final class CATransform3DInterpolationSnapshotTests: SnapshotTestCase { animation.addKeyframe(for: \.layer.transform, at: Double(index + 1) * segmentDuration, value: transform) } - animation.duration = TimeInterval(transforms.count) + animation.implicitDuration = TimeInterval(transforms.count) SnapshotVerify( animation: animation, diff --git a/Example/Unit Tests/CGAffineTransformInterpolationSnapshotTests.swift b/Example/Unit Tests/CGAffineTransformInterpolationSnapshotTests.swift index f404e2f..170bdc6 100644 --- a/Example/Unit Tests/CGAffineTransformInterpolationSnapshotTests.swift +++ b/Example/Unit Tests/CGAffineTransformInterpolationSnapshotTests.swift @@ -134,7 +134,7 @@ final class CGAffineTransformInterpolationSnapshotTests: SnapshotTestCase { animation.addKeyframe(for: \.transform, at: Double(index + 1) * segmentDuration, value: transform) } - animation.duration = TimeInterval(transforms.count) + animation.implicitDuration = TimeInterval(transforms.count) SnapshotVerify( animation: animation, diff --git a/Example/Unit Tests/DisplayLinkDriverTests.swift b/Example/Unit Tests/DisplayLinkDriverTests.swift index 38d7818..51dc918 100644 --- a/Example/Unit Tests/DisplayLinkDriverTests.swift +++ b/Example/Unit Tests/DisplayLinkDriverTests.swift @@ -26,7 +26,7 @@ final class DisplayLinkDriverTests: XCTestCase { let driver = DisplayLinkDriver( delay: 0, duration: 0, - repeatStyle: .none, + repeatStyle: .noRepeat, completion: nil ) @@ -56,7 +56,7 @@ final class DisplayLinkDriverTests: XCTestCase { let driver = DisplayLinkDriver( delay: 1, duration: 0, - repeatStyle: .none, + repeatStyle: .noRepeat, completion: nil, displayLinkFactory: { _, _ in displayLink } ) @@ -103,7 +103,7 @@ final class DisplayLinkDriverTests: XCTestCase { let driver = DisplayLinkDriver( delay: 0, duration: 1, - repeatStyle: .none, + repeatStyle: .noRepeat, completion: nil, displayLinkFactory: { _, _ in displayLink } ) @@ -164,7 +164,7 @@ final class DisplayLinkDriverTests: XCTestCase { let driver = DisplayLinkDriver( delay: 0, duration: 1, - repeatStyle: .none, + repeatStyle: .noRepeat, completion: nil, displayLinkFactory: { _, _ in displayLink } ) @@ -202,7 +202,7 @@ final class DisplayLinkDriverTests: XCTestCase { let driver = DisplayLinkDriver( delay: 1, duration: 4, - repeatStyle: .none, + repeatStyle: .noRepeat, completion: nil, displayLinkFactory: { _, _ in displayLink } ) @@ -568,7 +568,7 @@ final class DisplayLinkDriverTests: XCTestCase { let driver = DisplayLinkDriver( delay: 0, duration: 1, - repeatStyle: .none, + repeatStyle: .noRepeat, completion: completion, displayLinkFactory: { _, _ in displayLink } ) @@ -603,7 +603,7 @@ final class DisplayLinkDriverTests: XCTestCase { let driver = DisplayLinkDriver( delay: 0, duration: 1, - repeatStyle: .none, + repeatStyle: .noRepeat, completion: nil, displayLinkFactory: { _, _ in displayLink } ) @@ -710,7 +710,7 @@ final class DisplayLinkDriverTests: XCTestCase { let driver = DisplayLinkDriver( delay: 0, duration: 1, - repeatStyle: .none, + repeatStyle: .noRepeat, completion: nil, displayLinkFactory: { _, _ in displayLink } ) diff --git a/Sources/Stagehand/Animation/Animation.swift b/Sources/Stagehand/Animation/Animation.swift index 35aa7a1..836db92 100644 --- a/Sources/Stagehand/Animation/Animation.swift +++ b/Sources/Stagehand/Animation/Animation.swift @@ -62,23 +62,23 @@ public struct Animation { /// The duration of the animation. /// /// More specifically, this is the duration of one cycle of the animation. An animation that repeats will take a - /// total duration equal to the duration of one cycle (the animation's `duration`) multiplied by the number of - /// cycles (as specified by the animation's `repeatStyle`). + /// total duration equal to the duration of one cycle (the animation's `implicitDuration`) multiplied by the number + /// of cycles (as specified by the animation's `implicitRepeatStyle`). /// - /// When animations are composed, the duration is controlled by the top-most parent animation. The `duration` of any - /// child animations are ignored. - public var duration: TimeInterval = 1 + /// When animations are composed, the duration is controlled by the top-most parent animation. This means that the + /// `implicitDuration` of any child animations are ignored. + public var implicitDuration: TimeInterval = 1 /// The way in which the animation should repeat. /// - /// When animations are composed, the repeat style is controlled by the top-most parent animation. The `repeatStyle` - /// of any child animations are ignored. - public var repeatStyle: AnimationRepeatStyle = .none + /// When animations are composed, the repeat style is controlled by the top-most parent animation. This means that + /// the `implicitRepeatStyle` of any child animations are ignored. + public var implicitRepeatStyle: AnimationRepeatStyle = .noRepeat /// The curve applied to the animation. /// - /// Curves in child animations are applied on top of the curve(s) already applied by their parent(s). This allows - /// each child animation to have a different animation curve. + /// Curves in child animations are applied on top of the curve already applied by their parent. This allows each + /// child animation to have a different animation curve. public var curve: AnimationCurve = LinearAnimationCurve() // MARK: - Internal Computed Properties @@ -270,7 +270,7 @@ public struct Animation { /// Add a child animation. /// - /// The `childAnimation`'s `duration` and `repeatStyle` will be ignored. + /// The `childAnimation`'s `implicitDuration` and `implicitRepeatStyle` will be ignored. /// /// Keyframes in a child animation for the same property as keyframes in the parent will be overridden by the values /// of the keyframes in the parent. @@ -376,20 +376,33 @@ public struct Animation { /// Perform the animation on the given `element`. /// + /// The duration for each cycle of the animation will be determined in order of preference by: + /// 1. An explicit duration, if provided via the `duration` parameter + /// 2. The animation's implicit duration, as specified by the `implicitDuration` property + /// + /// The repeat style for the animation will be determined in order of preference by: + /// 1. An explicit repeat style, if provided via the `repeatStyle` parameter + /// 2. The animation's implicit repeat style, as specified by the `implicitRepeatStyle` property + /// /// - parameter element: The element to be animated. /// - parameter delay: The time interval to wait before performing the animation. + /// - parameter duration: The duration to use for each cycle the animation. + /// - parameter repeatStyle: The repeat style to use for the animation. /// - parameter completion: The completion block to call when the animation has concluded, with a parameter /// indicated whether the animation completed (as opposed to being cancelled). + /// - returns: An animation instance that can be used to check the status of or cancel the animation. @discardableResult public func perform( on element: ElementType, delay: TimeInterval = 0, + duration: TimeInterval? = nil, + repeatStyle: AnimationRepeatStyle? = nil, completion: ((_ finished: Bool) -> Void)? = nil ) -> AnimationInstance { let driver = DisplayLinkDriver( delay: delay, - duration: duration, - repeatStyle: repeatStyle, + duration: duration ?? implicitDuration, + repeatStyle: repeatStyle ?? implicitRepeatStyle, completion: completion ) @@ -507,7 +520,7 @@ public enum AnimationRepeatStyle: Equatable { case repeating(count: UInt, autoreversing: Bool) /// Animation will execute once. - public static let none: AnimationRepeatStyle = .repeating(count: 1, autoreversing: false) + public static let noRepeat: AnimationRepeatStyle = .repeating(count: 1, autoreversing: false) /// Animation will execute indefinitely (until canceled). /// - parameter autoreversing: Whether or not the animation should alternative direction on each cycle. The first @@ -663,7 +676,7 @@ extension Animation { /// only to store the keyframe series associated with it, since collapsing these into the parent would lose /// any animation curve applied to the child. /// - /// This animation's `duration` and `repeatStyle` can be ignored. + /// This animation's `implicitDuration` and `implicitRepeatStyle` can be ignored. var animation: Animation var relativeStartTimestamp: Double diff --git a/Sources/Stagehand/AnimationGroup.swift b/Sources/Stagehand/AnimationGroup.swift index c8ecbf4..f89c8ac 100644 --- a/Sources/Stagehand/AnimationGroup.swift +++ b/Sources/Stagehand/AnimationGroup.swift @@ -35,22 +35,26 @@ public struct AnimationGroup { // MARK: - Public Properties /// The duration of the animation group. - public var duration: TimeInterval { + /// + /// More specifically, this is the duration of one cycle of the animation group. An animation group that repeats + /// will take a total duration equal to the duration of one cycle (the animation group's `implicitDuration`) + /// multiplied by the number of cycles (as specified by the `implicitRepeatStyle`). + public var implicitDuration: TimeInterval { get { - return animation.duration + return animation.implicitDuration } set { - animation.duration = newValue + animation.implicitDuration = newValue } } /// The way in which the animation group should repeat. - public var repeatStyle: AnimationRepeatStyle { + public var implicitRepeatStyle: AnimationRepeatStyle { get { - return animation.repeatStyle + return animation.implicitRepeatStyle } set { - animation.repeatStyle = newValue + animation.implicitRepeatStyle = newValue } } @@ -78,7 +82,7 @@ public struct AnimationGroup { /// Add an animation to the group. /// - /// The `elementAnimation`'s `duration` and `repeatStyle` will be ignored. + /// The `elementAnimation`'s `implicitDuration` and `implicitRepeatStyle` will be ignored. /// /// - parameter elementAnimation: The animation to be performed on the `element`. /// - parameter element: The element to be animated. @@ -111,17 +115,32 @@ public struct AnimationGroup { /// Perform the animations in the group. /// + /// The duration for each cycle of the animation group will be determined in order of preference by: + /// 1. An explicit duration, if provided via the `duration` parameter + /// 2. The animation group's implicit duration, as specified by the `implicitDuration` property + /// + /// The repeat style for the animation group will be determined in order of preference by: + /// 1. An explicit repeat style, if provided via the `repeatStyle` parameter + /// 2. The animation group's implicit repeat style, as specified by the `implicitRepeatStyle` property + /// /// - parameter delay: The time interval to wait before performing the animation. - /// - parameter completion: The completion block to call when the animation has concluded, with a parameter + /// - parameter duration: The duration to use for each cycle the animation group. + /// - parameter repeatStyle: The repeat style to use for the animation group. + /// - parameter groupCompletion: The completion block to call when the animation has concluded, with a parameter /// indicated whether the animation completed (as opposed to being cancelled). + /// - returns: An animation instance that can be used to check the status of or cancel the animation group. @discardableResult public func perform( delay: TimeInterval = 0, + duration: TimeInterval? = nil, + repeatStyle: AnimationRepeatStyle? = nil, completion groupCompletion: ((_ finished: Bool) -> Void)? = nil ) -> AnimationInstance { return animation.perform( on: elementContainer, delay: delay, + duration: duration, + repeatStyle: repeatStyle, completion: { finished in self.completions.forEach { $0(finished) } groupCompletion?(finished) diff --git a/Sources/Stagehand/AnimationInstance/AnimationInstance.swift b/Sources/Stagehand/AnimationInstance/AnimationInstance.swift index b067e68..ad4f072 100644 --- a/Sources/Stagehand/AnimationInstance/AnimationInstance.swift +++ b/Sources/Stagehand/AnimationInstance/AnimationInstance.swift @@ -19,8 +19,9 @@ import Foundation /// An instance of an animation that has been triggered to begin. /// /// Do not create an `AnimationInstance` directly. Instead, construct an `Animation`, then call the animation's -/// `perform(on:delay:completion:)` method to begin the animation. That method will return an instance of this class. -/// The `AnimationInstance` can then be used to track the `status` of the animation, or to cancel it. +/// `perform(on:delay:duration:repeatStyle:completion:)` method to begin the animation. That method will return an +/// instance of this class. The `AnimationInstance` can then be used to track the `status` of the animation, or to +/// cancel it. public final class AnimationInstance { // MARK: - Life Cycle diff --git a/Sources/Stagehand/AnimationQueue.swift b/Sources/Stagehand/AnimationQueue.swift index 191bcdb..6757c15 100644 --- a/Sources/Stagehand/AnimationQueue.swift +++ b/Sources/Stagehand/AnimationQueue.swift @@ -37,12 +37,29 @@ public final class AnimationQueue { /// /// If the queue was previously empty, the animation will begin immediately. If the queue was previously not empty, /// the animation will begin when the last animation in the queue has completed. + /// + /// The duration for each cycle of the animation will be determined in order of preference by: + /// 1. An explicit duration, if provided via the `duration` parameter + /// 2. The animation's implicit duration, as specified by the animation's `implicitDuration` property + /// + /// The repeat style for the animation will be determined in order of preference by: + /// 1. An explicit repeat style, if provided via the `repeatStyle` parameter + /// 2. The animation's implicit repeat style, as specified by the animation's `implicitRepeatStyle` property + /// + /// - parameter animation: The animation to add to the queue. + /// - parameter duration: The duration to use for each cycle of the animation. + /// - parameter repeatStyle: The repeat style to use for the animation. + /// - returns: An animation instance that can be used to check the status of or cancel the animation. @discardableResult - public func enqueue(animation: Animation) -> AnimationInstance { + public func enqueue( + animation: Animation, + duration: TimeInterval? = nil, + repeatStyle: AnimationRepeatStyle? = nil + ) -> AnimationInstance { let driver = DisplayLinkDriver( delay: 0, - duration: animation.duration, - repeatStyle: animation.repeatStyle, + duration: duration ?? animation.implicitDuration, + repeatStyle: repeatStyle ?? animation.implicitRepeatStyle, completion: nil ) diff --git a/Sources/StagehandTesting/FBSnapshotTestCase+AnimationGIF.swift b/Sources/StagehandTesting/FBSnapshotTestCase+AnimationGIF.swift index 9984080..05988f4 100644 --- a/Sources/StagehandTesting/FBSnapshotTestCase+AnimationGIF.swift +++ b/Sources/StagehandTesting/FBSnapshotTestCase+AnimationGIF.swift @@ -86,7 +86,7 @@ extension FBSnapshotTestCase { ) let includeReverseCycle: Bool - switch animation.repeatStyle { + switch animation.implicitRepeatStyle { case let .repeating(count: count, autoreversing: autoreversing): includeReverseCycle = (count != 1 && autoreversing) } @@ -94,7 +94,7 @@ extension FBSnapshotTestCase { SnapshotVerify( animationInstance: animationInstance, using: element, - animationDuration: animation.duration, + animationDuration: animation.implicitDuration, includeReverseCycle: includeReverseCycle, fps: fps, bookendFrameDuration: bookendFrameDuration, @@ -146,7 +146,7 @@ extension FBSnapshotTestCase { ) let includeReverseCycle: Bool - switch animation.repeatStyle { + switch animation.implicitRepeatStyle { case let .repeating(count: count, autoreversing: autoreversing): includeReverseCycle = (count != 1 && autoreversing) } @@ -154,7 +154,7 @@ extension FBSnapshotTestCase { SnapshotVerify( animationInstance: animationInstance, using: view, - animationDuration: animation.duration, + animationDuration: animation.implicitDuration, includeReverseCycle: includeReverseCycle, fps: fps, bookendFrameDuration: bookendFrameDuration, @@ -206,7 +206,7 @@ extension FBSnapshotTestCase { ) let includeReverseCycle: Bool - switch animationGroup.repeatStyle { + switch animationGroup.implicitRepeatStyle { case let .repeating(count: count, autoreversing: autoreversing): includeReverseCycle = (count != 1 && autoreversing) } @@ -214,7 +214,7 @@ extension FBSnapshotTestCase { SnapshotVerify( animationInstance: animationInstance, using: view, - animationDuration: animationGroup.duration, + animationDuration: animationGroup.implicitDuration, includeReverseCycle: includeReverseCycle, fps: fps, bookendFrameDuration: bookendFrameDuration,