Andy Regensky

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 Timers 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 Timers 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.

Tagged with: