From 8ca866c52752acf97a44fd70d415982dba870a42 Mon Sep 17 00:00:00 2001 From: Dark-Existed Date: Thu, 30 Oct 2025 14:26:53 +0800 Subject: [PATCH 1/5] Add RepeatAnimation --- .../Animation/RepeatAnimationExample.swift | 28 +++ .../Animation/RepeatAnimiation.swift | 175 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 Example/SharedExample/Animation/Animation/RepeatAnimationExample.swift create mode 100644 Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift diff --git a/Example/SharedExample/Animation/Animation/RepeatAnimationExample.swift b/Example/SharedExample/Animation/Animation/RepeatAnimationExample.swift new file mode 100644 index 000000000..45fa5adb1 --- /dev/null +++ b/Example/SharedExample/Animation/Animation/RepeatAnimationExample.swift @@ -0,0 +1,28 @@ +// +// RepeatAnimationExample.swift +// SharedExample + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +struct RepeatAnimationExample: View { + @State var yOffset = 0.0 + + var body: some View { + Color.blue + .frame(width: 80, height: 50) + .offset(y: yOffset) + .onAppear { + withAnimation( + .linear + .speed(0.1) + .repeatCount(2, autoreverses: false) + ) { + yOffset = 100 + } + } + } +} diff --git a/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift b/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift new file mode 100644 index 000000000..5b8fb8220 --- /dev/null +++ b/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift @@ -0,0 +1,175 @@ +// +// RepeatAnimation.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 245300C05C5988649DCF97F905A0452C (SwiftUICore) + +import Foundation + +extension Animation { + /// Repeats the animation for a specific number of times. + /// + /// Use this method to repeat the animation a specific number of times. For + /// example, in the following code, the animation moves a truck from one + /// edge of the view to the other edge. It repeats this animation three + /// times. + /// + /// struct ContentView: View { + /// @State private var driveForward = true + /// + /// private var driveAnimation: Animation { + /// .easeInOut + /// .repeatCount(3, autoreverses: true) + /// .speed(0.5) + /// } + /// + /// var body: some View { + /// VStack(alignment: driveForward ? .leading : .trailing, spacing: 40) { + /// Image(systemName: "box.truck") + /// .font(.system(size: 48)) + /// .animation(driveAnimation, value: driveForward) + /// + /// HStack { + /// Spacer() + /// Button("Animate") { + /// driveForward.toggle() + /// } + /// Spacer() + /// } + /// } + /// } + /// } + /// + /// @Video(source: "animation-16-repeat-count.mp4", poster: "animation-16-repeat-count.png", alt: "A video that shows a box truck moving from the leading edge of a view to the trailing edge, and back again before looping in the opposite direction.") + /// + /// The first time the animation runs, the truck moves from the leading + /// edge to the trailing edge of the view. The second time the animation + /// runs, the truck moves from the trailing edge to the leading edge + /// because `autoreverse` is `true`. If `autoreverse` were `false`, the + /// truck would jump back to leading edge before moving to the trailing + /// edge. The third time the animation runs, the truck moves from the + /// leading to the trailing edge of the view. + /// + /// - Parameters: + /// - repeatCount: The number of times that the animation repeats. Each + /// repeated sequence starts at the beginning when `autoreverse` is + /// `false`. + /// - autoreverses: A Boolean value that indicates whether the animation + /// sequence plays in reverse after playing forward. Autoreverse counts + /// towards the `repeatCount`. For instance, a `repeatCount` of one plays + /// the animation forward once, but it doesn’t play in reverse even if + /// `autoreverse` is `true`. When `autoreverse` is `true` and + /// `repeatCount` is `2`, the animation moves forward, then reverses, then + /// stops. + /// - Returns: An animation that repeats for specific number of times. + public func repeatCount(_ repeatCount: Int, autoreverses: Bool = true) -> Animation { + modifier(RepeatAnimation(repeatCount: repeatCount, autoreverses: autoreverses)) + } + + /// Repeats the animation for the lifespan of the view containing the + /// animation. + /// + /// Use this method to repeat the animation until the instance of the view + /// no longer exists, or the view’s explicit or structural identity + /// changes. For example, the following code continuously rotates a + /// gear symbol for the lifespan of the view. + /// + /// struct ContentView: View { + /// @State private var rotationDegrees = 0.0 + /// + /// private var animation: Animation { + /// .linear + /// .speed(0.1) + /// .repeatForever(autoreverses: false) + /// } + /// + /// var body: some View { + /// Image(systemName: "gear") + /// .font(.system(size: 86)) + /// .rotationEffect(.degrees(rotationDegrees)) + /// .onAppear { + /// withAnimation(animation) { + /// rotationDegrees = 360.0 + /// } + /// } + /// } + /// } + /// + /// @Video(source: "animation-17-repeat-forever.mp4", poster: "animation-17-repeat-forever.png", alt: "A video that shows a gear that continuously rotates clockwise.") + /// + /// - Parameter autoreverses: A Boolean value that indicates whether the + /// animation sequence plays in reverse after playing forward. + /// - Returns: An animation that continuously repeats. + public func repeatForever(autoreverses: Bool = true) -> Animation { + modifier(RepeatAnimation(repeatCount: nil, autoreverses: autoreverses)) + } +} + +struct RepeatAnimation: CustomAnimationModifier { + var repeatCount: Int? + var autoreverses: Bool + + func animate( + base: B, + value: V, + time: TimeInterval, + context: inout AnimationContext + ) -> V? where V: VectorArithmetic, B: CustomAnimation { + let state = context.repeatState + let elapsed = time - state.timeOffset + let reversed = autoreverses && (state.index & 1 != 0) + guard let newValue = base.animate(value: value, time: elapsed, context: &context) else { + let index = state.index &+ 1 + context.repeatState = .init(index: index, timeOffset: time) + if let repeatCount, index >= repeatCount { + return nil + } + return reversed ? .zero : value + } + return reversed ? value - newValue : newValue + } + + func function(base: Animation.Function) -> Animation.Function { + base + } +} + + extension RepeatAnimation: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + if let repeatCount { + encoder.intField(1, repeatCount) + } + encoder.boolField(2, autoreverses) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var repeatCount: Int? = nil + var autoreverses: Bool = true + while let field = try decoder.nextField() { + switch field.tag { + case 1: repeatCount = try decoder.intField(field) + case 2: autoreverses = try decoder.boolField(field) + default: try decoder.skipField(field) + } + } + self.init(repeatCount: repeatCount, autoreverses: autoreverses) + } +} + +extension AnimationContext { + fileprivate var repeatState: RepeatState { + get { state[RepeatState.self] } + set { state[RepeatState.self] = newValue } + } +} + +private struct RepeatState: AnimationStateKey where Value: VectorArithmetic { + static var defaultValue: RepeatState { + RepeatState(index: 0, timeOffset: 0) + } + + var index: Int + var timeOffset: Double +} From 08dcbc0bd0aaa3feea13d6216b952ddb76c1b56a Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 10 Nov 2025 01:04:53 +0800 Subject: [PATCH 2/5] [NFC] Optimize code format --- .../Animation/RepeatAnimiation.swift | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift b/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift index 5b8fb8220..f87b264da 100644 --- a/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift +++ b/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift @@ -8,7 +8,10 @@ import Foundation +// MARK: - View + repeat Animation + extension Animation { + /// Repeats the animation for a specific number of times. /// /// Use this method to repeat the animation a specific number of times. For @@ -107,8 +110,11 @@ extension Animation { } } +// MARK: - RepeatAnimation + struct RepeatAnimation: CustomAnimationModifier { var repeatCount: Int? + var autoreverses: Bool func animate( @@ -136,40 +142,30 @@ struct RepeatAnimation: CustomAnimationModifier { } } - extension RepeatAnimation: ProtobufMessage { - package func encode(to encoder: inout ProtobufEncoder) throws { + extension RepeatAnimation: ProtobufEncodableMessage { + func encode(to encoder: inout ProtobufEncoder) throws { if let repeatCount { encoder.intField(1, repeatCount) } encoder.boolField(2, autoreverses) } - - package init(from decoder: inout ProtobufDecoder) throws { - var repeatCount: Int? = nil - var autoreverses: Bool = true - while let field = try decoder.nextField() { - switch field.tag { - case 1: repeatCount = try decoder.intField(field) - case 2: autoreverses = try decoder.boolField(field) - default: try decoder.skipField(field) - } - } - self.init(repeatCount: repeatCount, autoreverses: autoreverses) - } } -extension AnimationContext { - fileprivate var repeatState: RepeatState { - get { state[RepeatState.self] } - set { state[RepeatState.self] = newValue } - } -} +// MARK: - RepeatState -private struct RepeatState: AnimationStateKey where Value: VectorArithmetic { +struct RepeatState: AnimationStateKey where Value: VectorArithmetic { static var defaultValue: RepeatState { RepeatState(index: 0, timeOffset: 0) } var index: Int + var timeOffset: Double } + +extension AnimationContext { + fileprivate var repeatState: RepeatState { + get { state[RepeatState.self] } + set { state[RepeatState.self] = newValue } + } +} From c67d210e8b7ea89936e34bc587b9c790527508e6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 10 Nov 2025 01:33:24 +0800 Subject: [PATCH 3/5] Fix RepeatAnimation encode implementation --- .../Animation/EncodableAnimation.swift | 17 +++++++++++++++++ .../Animation/Animation/RepeatAnimiation.swift | 11 +---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Sources/OpenSwiftUICore/Animation/Animation/EncodableAnimation.swift b/Sources/OpenSwiftUICore/Animation/Animation/EncodableAnimation.swift index 1e5dd8b7b..23e995173 100644 --- a/Sources/OpenSwiftUICore/Animation/Animation/EncodableAnimation.swift +++ b/Sources/OpenSwiftUICore/Animation/Animation/EncodableAnimation.swift @@ -47,6 +47,23 @@ extension FluidSpringAnimation: EncodableAnimation { } } +extension RepeatAnimation: ProtobufEncodableMessage { + func encode(to encoder: inout ProtobufEncoder) throws { + encoder.messageField(5) { encoder in + if let repeatCount { + encoder.intField(1, repeatCount, defaultValue: .min) + } + encoder.boolField(2, autoreverses) + } + } +} + +extension SpeedAnimation: ProtobufEncodableMessage { + func encode(to encoder: inout ProtobufEncoder) throws { + encoder.doubleField(6, speed) + } +} + extension DefaultAnimation: EncodableAnimation { package static var leafProtobufTag: CodableAnimation.Tag? { .init(rawValue: 7) diff --git a/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift b/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift index f87b264da..436d7662c 100644 --- a/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift +++ b/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift @@ -142,15 +142,6 @@ struct RepeatAnimation: CustomAnimationModifier { } } - extension RepeatAnimation: ProtobufEncodableMessage { - func encode(to encoder: inout ProtobufEncoder) throws { - if let repeatCount { - encoder.intField(1, repeatCount) - } - encoder.boolField(2, autoreverses) - } -} - // MARK: - RepeatState struct RepeatState: AnimationStateKey where Value: VectorArithmetic { @@ -159,7 +150,7 @@ struct RepeatState: AnimationStateKey where Value: VectorArithmetic { } var index: Int - + var timeOffset: Double } From 14ffb31576e28361531182b3119c11b7aa45fdf1 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 10 Nov 2025 01:45:16 +0800 Subject: [PATCH 4/5] Add missing available mark and fix function implementation --- .../Animation/Animation/RepeatAnimiation.swift | 7 ++++++- .../Animation/Animation/SpeedAnimation.swift | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift b/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift index 436d7662c..8af369460 100644 --- a/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift +++ b/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift @@ -10,6 +10,7 @@ import Foundation // MARK: - View + repeat Animation +@available(OpenSwiftUI_v1_0, *) extension Animation { /// Repeats the animation for a specific number of times. @@ -138,7 +139,11 @@ struct RepeatAnimation: CustomAnimationModifier { } func function(base: Animation.Function) -> Animation.Function { - base + .repeat( + count: repeatCount.map { Double($0) } ?? .infinity, + autoreverses: autoreverses, + base + ) } } diff --git a/Sources/OpenSwiftUICore/Animation/Animation/SpeedAnimation.swift b/Sources/OpenSwiftUICore/Animation/Animation/SpeedAnimation.swift index 209036dd9..f3d22be5a 100644 --- a/Sources/OpenSwiftUICore/Animation/Animation/SpeedAnimation.swift +++ b/Sources/OpenSwiftUICore/Animation/Animation/SpeedAnimation.swift @@ -7,6 +7,8 @@ import Foundation +// MARK: - View + speed Animation + @available(OpenSwiftUI_v1_0, *) extension Animation { /// Changes the duration of an animation by adjusting its speed. @@ -79,6 +81,8 @@ extension Animation { } } +// MARK: - SpeedAnimation + struct SpeedAnimation: CustomAnimationModifier { var speed: Double @@ -117,6 +121,6 @@ struct SpeedAnimation: CustomAnimationModifier { } func function(base: Animation.Function) -> Animation.Function { - base + .speed(speed, base) } } From 5f3a49c7b170571f2095d95ab8bd831d4752cd7c Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 10 Nov 2025 02:29:31 +0800 Subject: [PATCH 5/5] Fix RepeatAnimation animate implementation --- .../Animation/Animation/RepeatAnimiation.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift b/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift index 8af369460..6d07ff7ec 100644 --- a/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift +++ b/Sources/OpenSwiftUICore/Animation/Animation/RepeatAnimiation.swift @@ -124,18 +124,19 @@ struct RepeatAnimation: CustomAnimationModifier { time: TimeInterval, context: inout AnimationContext ) -> V? where V: VectorArithmetic, B: CustomAnimation { - let state = context.repeatState - let elapsed = time - state.timeOffset - let reversed = autoreverses && (state.index & 1 != 0) + let repeatState = context.repeatState + let elapsed = time - repeatState.timeOffset + let isReversed = (repeatState.index % 2 == 1) && autoreverses guard let newValue = base.animate(value: value, time: elapsed, context: &context) else { - let index = state.index &+ 1 + context.state = .init() + let index = repeatState.index + 1 context.repeatState = .init(index: index, timeOffset: time) if let repeatCount, index >= repeatCount { return nil } - return reversed ? .zero : value + return isReversed ? .zero : value } - return reversed ? value - newValue : newValue + return isReversed ? value - newValue : newValue } func function(base: Animation.Function) -> Animation.Function {