Reputation: 6070
I want to repeat an animation forever, without it to autoreverse, and with a delay/pause between the repetitions after the animation played.
I know there is a .delay()
modifier, but it delays the beginning of the animation:
let ani = Animation.easeInOut.delay(1.0).repeatForever(autoreverses: false)
In addition with the .repeatForever
modifier, after the animation played it immediately jumps back to its beginning. But I want the last keyframe to remain visible on the screen for a short amount of time.
I’ve also tried it the other way around, adding a .delay()
after the .repeatForever
modifier, but without success (delay
has no effect).
let ani = Animation.easeInOut.repeatForever(autoreverses: false).delay(1.0)
How can I add a delay after the animation played?
Upvotes: 7
Views: 1601
Reputation: 21720
You might expect that a PhaseAnimator
with 2 phases could be used for this:
Unfortunately, it seems that when a delay is added to an animation with a duration of 0, the delay doesn't take effect.
A 3-phase animation with a hold phase doesn't work either. It seems that a phase animator skips any phases that don't actually make any changes.
So using a phase animator doesn't work.
An alternative approach (one that does work) is to use a KeyframeAnimator
instead. The animator just needs a single KeyframeTrack
, with 3 keyframes to set / hold / reset the change:
struct ContentView: View {
let circleSize: CGFloat = 100
var body: some View {
GeometryReader { proxy in
let containerWidth = proxy.size.width
let animationDuration = (containerWidth - circleSize) / 100.0
Circle()
.fill(.orange)
.frame(width: circleSize, height: circleSize)
.keyframeAnimator(initialValue: CGFloat.zero) { content, xOffset in
content
.offset(x: xOffset)
} keyframes: { xOffset in
KeyframeTrack {
LinearKeyframe(containerWidth - circleSize, duration: animationDuration) // set
LinearKeyframe(containerWidth - circleSize, duration: 1) // hold
LinearKeyframe(0, duration: 0) // reset
}
}
}
.frame(height: circleSize)
.padding()
}
}
Btw, if the reset would be performed with a very fast animation then a phase animator could in fact be used. The delay can be added to the animation used for phase 2. An example implementation of a 2-phase animation (without delay) can be found in this answer.
For earlier iOS versions (15+), a .task(id:)
modifier can be used to implement the animation phases manually. The following example works the same as the version above that used a keyframe animator.
struct ContentView: View {
let circleSize: CGFloat = 100
@State private var xOffset = CGFloat.zero
var body: some View {
GeometryReader { proxy in
let containerWidth = proxy.size.width
let animationDuration = (containerWidth - circleSize) / 100.0
Circle()
.fill()
.foregroundStyle(.orange)
.frame(width: circleSize, height: circleSize)
.offset(x: xOffset)
.animation(.linear(duration: xOffset > 0 ? animationDuration : 0), value: xOffset)
.task(id: xOffset) {
if xOffset > 0 {
try? await Task.sleep(for: .seconds(animationDuration + 1))
}
xOffset = xOffset > 0 ? 0 : containerWidth - circleSize
}
}
.frame(height: circleSize)
.padding()
}
}
An alternative approach for earlier iOS versions would be to use an Animatable
ViewModifier
instead. The answer referenced above has an example that animates the reset phase at a different speed. The reset phase can be changed to a "hold" phase with a small adaption:
private var xOffset: CGFloat {
let fraction: CGFloat = progress > rewindThreshold ? rewindThreshold : progress
return (fraction / rewindThreshold) * maxOffset
}
Upvotes: 0