Create an autorepeating fill-then-clear animation in SwiftUI
This article shows you how to animate a path in SwiftUI, so that in a first step it's filled from start to end, before being cleared from start to end in a second step. This provides a seamless autorepeating animation without having to rely on autoreversing behaviour. This kind of animation is somewhat hard to explain, but the following video does a much better job than any of my words could ever do.
Now that's a nice animation! And it can be used with arbitrary paths when it's set up. So how do we approach this? At first, it doesn't seem like a hard thing to do, it's just two animations chained together. However, while chaining animations in UIKit could be done with completion handlers, SwiftUI (as of March 2020) does not support chaining animations natively.
Let's break down what we need to do in order to create the desired animation. Step 1 is the creation of a path to animate, for simplicity we'll just use a Circle
in this article. Step 2 is the setup of both animations, fill and clear. Step 3 is the seamless combination of both animations, such that the overall animation is autorepeated.
Step 1: Create a shape
To start implementing the animation, we need a path that can be animated. We decide for Circle
and embed it into a SwiftUI View
. It is further embedded into a VStack
to push it to the top of the screen with the help of a Spacer
. Then, we stroke the path of the circle with a LinearGradient
and decide for a round line cap to make things a little more appealing. Finally, we define the frame size of our circle.
struct FillClearView: View {
var body: some View {
VStack {
Circle()
.stroke(LinearGradient(gradient: Gradient(colors: [.blue, .red]),
startPoint: .topLeading,
endPoint: .bottomTrailing),
style: .init(lineWidth: 4, lineCap: .round))
.frame(width: 320, height: 320)
Spacer()
}
}
}
Step 2: Setup the animations
For the animations, we need properties that can be animated. As we want to animate the start and the end of the stroke, we add two properties start
and end
to the view. For the fill animation, both properties should initially be set to 0, for the clear animation, end
should be set to 1 initially.
@State var start: CGFloat = 0
@State var end: CGFloat = 0
Then, we add the trim
modifier to the path before stroke
. By setting from
to start
and to
to end
, the path will be drawn between start
and end
. Thereby, the values of start
and end
lie between 0 and 1, where 0 represents the beginning of the path and 1 represents its end. Setting start
to 0 and end
to 0.5 for example, would lead to a half-circle being drawn.
Circle()
...
.trim(from: start, to: end)
.stroke(...)
...
The animation itself has to be set up within the onAppear
modifier, so that it starts automatically once the view appears. We add it to the modifier chain after defining the frame. For the fill animation, we animate end
from its initial value 0 to 1 and set the duration to 3 seconds. Go ahead and try to create the clear animation, which starts with a full circle and animates the start from 0 to 1.
.onAppear {
withAnimation(.linear(duration: 3)) {
self.end = 1
}
}
}
Step 3: Combine both animations
Combining both animations is not as easy as it may sound at first due to the way that SwiftUI animates properties. If you animate a property in withAnimation
, the value of the animated property changes instantly. Therefore, if you chain animations that depend on the state of each other - here, both animations depend on start
and end
- a second (delayed) animation may override property values that are necessery for the first animation. Hence, we need a different way to setup the desired autorepeating fill-then-clear animation.
For this to work, we make heavy use of Timer
s as they give us clear control over the timing of our animation and its autorepeating behaviour. Add a constant property duration
to the view defining the duration of the overall animation and replace the content of the onAppear
modifier with the following. Note, that the duration should be sufficiently large, otherwise Timer
s on an actual device are not accurate enough to obtain a smooth animation.
// duration property
let duration: TimeInterval = 3
// onAppear
.onAppear {
Timer.scheduledTimer(withTimeInterval: self.duration, // Fill Timer
repeats: true) { _ in
self.start = 0
self.end = 0
withAnimation(.linear(duration: self.duration/2)) {
self.end = 1
}
}.fire()
Timer.scheduledTimer(withTimeInterval: self.duration/2, // Delay Timer
repeats: false) { _ in
Timer.scheduledTimer(withTimeInterval: self.duration, // Clear Timer
repeats: true) { _ in
withAnimation(.linear(duration: self.duration/2)) {
self.start = 1
}
}.fire()
}
}
Three Timers
are necessary to set up the autorepeating fill-then-clear animation in SwiftUI. The two timers Fill and Clear take care of repeatedly starting the fill and clear animations, respectively. They fire every duration
seconds and are fired immediately upon creation to avoid an annoying delay between the user interface becoming visible and the start of the animation. The Delay timer takes care of the required offset between the Clear and the Fill timer. As each animation lasts for duration/2
seconds, the clear animation has to be delayed by duration/2
seconds in order to run once the fill animation completed. The Delay timer is only required for setting up that offset between the Clear and the Fill timer and does hence fire only once. Note, that the Fill timer additionally resets the properties start
and end
before starting the animation. This is required due to the autorepeating behaviour and can be dropped for one-time animations of this kind.
This autorepeating fill-then-clear animation can be used on arbitrary paths and is a great animation to convey running timers or background activities to your users. If you have any comments, questions or remarks, you spotted an error, or you just liked this article and want to get in touch, please message me on Twitter @andyregensky.