Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for dynamic animation durations #72

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
}

}
Loading