Skip to content

Commit

Permalink
Add support for dynamic animation durations
Browse files Browse the repository at this point in the history
  • Loading branch information
NickEntin committed Apr 12, 2024
1 parent d4cc5b5 commit 226dc2c
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 6 deletions.
2 changes: 1 addition & 1 deletion Example/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: b4841bd82e57283ff97d83f4bb89137cc01f6102

COCOAPODS: 1.11.3
COCOAPODS: 1.14.3
38 changes: 36 additions & 2 deletions Sources/Stagehand/Animation/Animation.swift
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -407,10 +407,44 @@ public struct Animation<ElementType: AnyObject> {
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
)
Expand Down
45 changes: 45 additions & 0 deletions Sources/Stagehand/Animation/AnimationDurationProvider.swift
Original file line number Diff line number Diff line change
@@ -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
}

}
35 changes: 35 additions & 0 deletions Sources/Stagehand/AnimationGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: -
Expand Down
32 changes: 31 additions & 1 deletion Sources/Stagehand/AnimationQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,40 @@ public final class AnimationQueue<ElementType: AnyObject> {
animation: Animation<ElementType>,
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<ElementType>,
durationProvider: AnimationDurationProvider,
repeatStyle: AnimationRepeatStyle? = nil
) -> AnimationInstance {
let driver = DisplayLinkDriver(
delay: 0,
duration: duration ?? animation.implicitDuration,
durationProvider: durationProvider,
repeatStyle: repeatStyle ?? animation.implicitRepeatStyle,
completion: nil
)
Expand Down
20 changes: 18 additions & 2 deletions Sources/Stagehand/Driver/DisplayLinkDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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

Expand Down
32 changes: 32 additions & 0 deletions Sources/Stagehand/Utilities/Lazy.swift
Original file line number Diff line number Diff line change
@@ -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<Value> {

init(wrappedValue: @autoclosure @escaping () -> Value) {
self.factory = wrappedValue
}

let factory: () -> Value

var wrappedValue: Value {
return factory()
}

}

0 comments on commit 226dc2c

Please sign in to comment.