SwiftUI: Spring Animation Completion Tracker
I want to share my solution for tracking animation completion in SwiftUI, which effectively handles various types of animations, including spring animations.
I was inspired by this great article “withAnimation completion callback with animatable modifiers” that suggests the idea of triggering a completion callback by monitoring the animatable parameter and waiting for it to reach the target value. However, for those who strive for perfection (which I occasionally do), this approach may not be entirely satisfying. The reason is that it relies on the assumption that the animation parameter will always be distinct from the target value throughout the animation. This assumption is generally applicable to most commonly used animation curves, except for spring animations.
As you can see from the picture above, there’s a probability of the spring animation parameter hitting the target value in one of the oscillations, which means false positives (* The discrete nature of the representation of numerical values makes this not improbable). Even though it’s quite small, and the suggested method can mostly work as an acceptable workaround, for spring animations many people prefer to fallback to manually firing the completion handler after an empirically determined delay.
My approach is based on a little insight on how animation actually works. Animation is essentially implemented through a smooth transformation of the animation parameter from the original value, to the target value, which is divided into a finite number of moments in time — keyframes. The key aspect of animation lies in the progressive change of the parameter during the animation process, meaning that the animation parameter will always be distinct at two adjacent keyframes. Consequently, we can identify the end of the animation when there is no change between adjacent keyframes! In my experiments, this method has proven effective, but there is a possibility of false positives occurring when, for example, the highest or lowest point of the animation curve falls between adjacent keyframes. To ensure reliability, it is necessary to consider not just two adjacent values but three (as shown in Picture 2)!
Here is the code that demonstrates this solution:
And also the convenience code for using this view modifier:
In this code, I use a timer for simplicity, with the expectation that it will trigger every keyframe (* Frames typically last 16.7 milliseconds). However, in reality, the timer is not prioritized enough and may be triggered less frequently, especially in the middle of the animation. This makes it even more reasonable to check for 3 contiguous values instead of just 2. After the animation completion, the timer’s triggering rate tends to become more satisfactory, and in my experiments, it accurately determined the end of the animation.
Ideally, it would be preferable to achieve checking the contiguous animation parameter values at the end of processing each frame. However, this is beyond the scope of this article. If you are interested in an out-of-the-box solution, see this repository (AnimationStatusReporterModifier.swift).
To summarize, this universal method is capable of determining the completion of any animation, including spring animations. The main drawback of this approach is that it requires 2 extra frames to determine the state of the animation. This hasn’t really made a difference in my animations so far, but if you have a better idea, I’d be happy to hear about it.