Having done a few animations with SwiftUI, I have realized how tough it is to use the
withAnimation() modifier on multiple properties and views at different times or sequentially. Besides, when it comes to moving views around in a very particular way
withAnimation() may lack clarity, extensibility, and sometimes organization. That’s why I’m going to show you the
AnimationSequence framework I’ve written that wraps the beauty of
SwiftUI.Animation and declutters our code a bit. Let’s dive into it.
1 — Current scenario
This is what I normally find when looking for tutorials on Google or YouTube on how to animate views sequentially. I’ll be explaining what is going on within this snippet, but first, let’s check it out, shall we?
Ok, what do we have here? let’s see what’s happening with this code, why it can be painful and expensive when it comes to updates or extensions. The code snippet is:
- keeping track of booleans that tell when a view is being animated.
- using separate booleans for each view that we want to animate differently (or in a different moment).
- using booleans for different properties we want to animate.
- using boolean in a ternary operator to set a different value depending on whether it’s on or off.
- and delaying animations based on the previous durations.
Imagine that now, we need to change any animation duration, or we want to move some views horizontally instead of vertically, or we’d like to add a delay to one of the animations, or just as simple as grouping animations together. Even though the approach suggested by the code above is not bad for smaller examples, it is going to cost a lot of refactoring and probably a headache while dealing with more complex scenarios.
2 — Adding an animation state
To get our terrain ready, let’s first add an animation state where we keep track of everything that’s happening to our views, such as colors, offsets, positions, borders, scales, etc. In this step, we’ll clean up the ContentView and have more flexibility on which value to set for each specific property. A little disclaimer before we move forward: This is my approach to solving this problem, and it is far from being perfect or the best solution, it’s just what I’ve found more fun to work with.
As we can see in the above snippet, introducing the
AnimationState removes lots of smells and makes our
View looks cleaner, however, animating the views is still a fairly large amount of work, keeping track of animation durations and deciding the order of those animations, I am confident to say it is a painful task, considering this animation only consists in moving three circles up and down and then change their color.
3 — Let’s finally see the animation sequence
After this huge introduction to the problem, to the current situation, and cleaning up the stage a little, let’s talk about what brings us to this article — how to write a nice animation sequence without caring too much about delays, animation order, or crazy details other than the actual animation definitions, such as — move this view up by 20 pixels, change the background color of this view, increase the scale of this other view to 2, etc.
Said that we can picture a non-existent animation sequence code(because we haven’t written it) that looks clean and handy — of course, imaginary code within the Swift syntax and what is humanly possible.
There is a couple of things we can highlight from this imaginary snippet:
- The code looks cleaner than before since we got rid of unnecessary calculations of delays.
- We set a default duration to
0.5and easing to
.defaultfor all of our concatenated animations. // 1.
- We give specific animation values to every block we append to the sequence. // 2.
- To finish our sequence definition and start animating, we call the function
4 — Writing the solution
As you might have imagined already, we require
AnimationSequence to follow a builder pattern in which it returns itself every time we call the function append. It should also receive the optional values
easing, and a
non-optional trailing closure with the block of code that we want to execute later within the actual
withAnimation() block. Let’s see the builder.
To save all animation values, let me introduce to you the
AnimationConfig structure that holds duration, delay, easing, and a block/closure.
Now, we are ready to write the
start() function. Based on all the animations we’ve appended, it creates a sequence by dispatching every animation with a time offset, which means, that the duration of the previous one is the delay of the current same process for every animation we have. For instance, let’s say we have an initial
timeOffset=0 and when the first animation A comes, timeOffset increases its value by
A.duration + A.delay, hence dispatching the next animation B
timeOffset seconds later.
5 — Putting everything together
After a decent amount of time reading code (if you didn’t skip it), I hand to you the whole implementation of AnimationSequence, it’s not much different from the snippets we’ve seen so far, it’s just a more complete piece of code, I’ve added some debug flags, asynchronous tasks, an
onFinish callback, I split the code into some files and did some cleaning up in general.
Thanks so much for reading. I hope you have enjoyed this small piece of code, and if it was useful for you, don’t be shy to 👏 on this article or just leave me a comment telling me how you liked it. See you next time.