diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 1df14d7..2053b57 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -38,4 +38,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: b4841bd82e57283ff97d83f4bb89137cc01f6102 -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 diff --git a/Sources/Stagehand/Animation/Animation.swift b/Sources/Stagehand/Animation/Animation.swift index ac8a3ab..b36f925 100644 --- a/Sources/Stagehand/Animation/Animation.swift +++ b/Sources/Stagehand/Animation/Animation.swift @@ -1,5 +1,5 @@ // -// Copyright 2019 Square Inc. +// Copyright 2024 Block Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -407,10 +407,44 @@ public struct Animation { duration: TimeInterval? = nil, repeatStyle: AnimationRepeatStyle? = nil, completion: ((_ finished: Bool) -> Void)? = nil + ) -> AnimationInstance { + return perform( + on: element, + delay: delay, + durationProvider: FixedDurationProvider(duration: duration ?? implicitDuration), + repeatStyle: repeatStyle, + completion: completion + ) + } + + /// 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 durationProvider: The duration provider to use for 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, + durationProvider: AnimationDurationProvider, + repeatStyle: AnimationRepeatStyle? = nil, + completion: ((_ finished: Bool) -> Void)? = nil ) -> AnimationInstance { let driver = DisplayLinkDriver( delay: delay, - duration: duration ?? implicitDuration, + duration: durationProvider.nextInstanceDuration(), repeatStyle: repeatStyle ?? implicitRepeatStyle, completion: completion ) diff --git a/Sources/Stagehand/Animation/AnimationDurationProvider.swift b/Sources/Stagehand/Animation/AnimationDurationProvider.swift new file mode 100644 index 0000000..44b6081 --- /dev/null +++ b/Sources/Stagehand/Animation/AnimationDurationProvider.swift @@ -0,0 +1,45 @@ +// +// Copyright 2024 Block Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public protocol AnimationDurationProvider { + + func nextInstanceDuration() -> TimeInterval + +} + +// MARK: - + +public struct FixedDurationProvider: AnimationDurationProvider { + + // MARK: - Life Cycle + + public init(duration: TimeInterval) { + self.duration = duration + } + + // MARK: - Public Properties + + public let duration: TimeInterval + + // MARK: - AnimationDurationProvider + + public func nextInstanceDuration() -> TimeInterval { + return duration + } + +} diff --git a/Sources/Stagehand/AnimationGroup.swift b/Sources/Stagehand/AnimationGroup.swift index f89c8ac..b0533bd 100644 --- a/Sources/Stagehand/AnimationGroup.swift +++ b/Sources/Stagehand/AnimationGroup.swift @@ -148,6 +148,41 @@ 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 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, + durationProvider: AnimationDurationProvider, + repeatStyle: AnimationRepeatStyle? = nil, + completion groupCompletion: ((_ finished: Bool) -> Void)? = nil + ) -> AnimationInstance { + return animation.perform( + on: elementContainer, + delay: delay, + durationProvider: durationProvider, + repeatStyle: repeatStyle, + completion: { finished in + self.completions.forEach { $0(finished) } + groupCompletion?(finished) + } + ) + } + } // MARK: - diff --git a/Sources/Stagehand/AnimationQueue.swift b/Sources/Stagehand/AnimationQueue.swift index 0a86fff..ec6608b 100644 --- a/Sources/Stagehand/AnimationQueue.swift +++ b/Sources/Stagehand/AnimationQueue.swift @@ -70,10 +70,40 @@ public final class AnimationQueue { animation: Animation, duration: TimeInterval? = nil, repeatStyle: AnimationRepeatStyle? = nil + ) -> AnimationInstance { + return enqueue( + animation: animation, + durationProvider: FixedDurationProvider(duration: duration ?? animation.implicitDuration), + repeatStyle: repeatStyle + ) + } + + /// Adds the animation to the queue. + /// + /// 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 durationProvider: The duration provider to use for 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, + durationProvider: AnimationDurationProvider, + repeatStyle: AnimationRepeatStyle? = nil ) -> AnimationInstance { let driver = DisplayLinkDriver( delay: 0, - duration: duration ?? animation.implicitDuration, + durationProvider: durationProvider, repeatStyle: repeatStyle ?? animation.implicitRepeatStyle, completion: nil ) diff --git a/Sources/Stagehand/Driver/DisplayLinkDriver.swift b/Sources/Stagehand/Driver/DisplayLinkDriver.swift index f812e7f..781409d 100644 --- a/Sources/Stagehand/Driver/DisplayLinkDriver.swift +++ b/Sources/Stagehand/Driver/DisplayLinkDriver.swift @@ -30,7 +30,22 @@ internal final class DisplayLinkDriver: Driver { displayLinkFactory: DisplayLinkFactory = CADisplayLink.init(target:selector:) ) { self.delay = delay - self.duration = (duration * systemAnimationCoefficient()) + self._duration = Lazy(wrappedValue: duration * systemAnimationCoefficient()) + self.repeatStyle = repeatStyle + self.completions = [completion].compactMap { $0 } + + self.displayLink = displayLinkFactory(self, #selector(renderCurrentFrame)) + } + + internal init( + delay: TimeInterval, + durationProvider: AnimationDurationProvider, + repeatStyle: AnimationRepeatStyle, + completion: ((Bool) -> Void)?, + displayLinkFactory: DisplayLinkFactory = CADisplayLink.init(target:selector:) + ) { + self.delay = delay + self._duration = Lazy(wrappedValue: durationProvider.nextInstanceDuration() * systemAnimationCoefficient()) self.repeatStyle = repeatStyle self.completions = [completion].compactMap { $0 } @@ -59,7 +74,8 @@ internal final class DisplayLinkDriver: Driver { private let delay: TimeInterval - private let duration: TimeInterval + @Lazy + private var duration: TimeInterval private let repeatStyle: AnimationRepeatStyle diff --git a/Sources/Stagehand/Utilities/Lazy.swift b/Sources/Stagehand/Utilities/Lazy.swift new file mode 100644 index 0000000..317f262 --- /dev/null +++ b/Sources/Stagehand/Utilities/Lazy.swift @@ -0,0 +1,32 @@ +// +// Copyright 2024 Square Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@propertyWrapper +struct Lazy { + + init(wrappedValue: @autoclosure @escaping () -> Value) { + self.factory = wrappedValue + } + + let factory: () -> Value + + var wrappedValue: Value { + return factory() + } + +}