Hello and welcome to another blog post about SwiftUI. This time, we will talk about the animation of complex shapes in SwiftUI. You will understand animatableData property and will be able to implement animatable custom Shape struct that depends on multiple parameters.
Animating simple shapes is easy thanks to animatableData property. We have seen an example of such animation in my previous post about custom controls. The magic behind animatableData is actually quite simple math. During the animation, the property value is being interpolated (or extrapolated in case of spring animation) from starting to the ending value according to the animation timing curve.
To demonstrate it once again, let’s start with a simple rectangle with cout-out rounded corners.
Notice that I have created a single property called cornerRadius that is being set or read from animatableData setter and getter. That is enough for SwiftUI to perform the animation whenever we change the corner radius.
Let’s test it within a simple demo view that periodically animates corners from value 0 to 20 and back.
You can try to change the animation type or its parameters to see how the animation changes.
Now, in the case things are more complicated and we need to animate the shape according to two values, we can utilize AnimatablePair<T> type. Here the only obstacle is to define the right getter and setter to pass data between our control properties and values stored in AnimatablePair.
As an example, we will build a wedge shape that can be used to compose pie charts. The wedge has two main properties - angleOffset and wedgeWidth that we both want to be animatable. The best explanation of both properties gives the following illustration:
And the resulting code is here. Note how AnimatablePair`s first and second values are being mapped to our properties.
Having wedge shape ready, it is now possible to compose a ZStack of these views to create an animatable pie chart, like this:
The obvious question appears. What if our view depends on multiple values and we wish to animate according to all of them? There are two solutions.
First, it is possible to cascade multiple AnimatablePairs to pass more (like 3 or 4) values. Something like this:
This is actually used within the implementation of EdgeInsets type, but as you can guess, it is not very flexible and usable for more than five or six values.
Luckily, there is a better approach. As animatableData can be set any type that implements to VectorArithmetic protocol. As we have seen, it is implemented by basic scalar types like Double or CGFloat and of course by AnimatablePair.
Knowing that, let us implement a brand new type called AnimatableVector that will be able to hold up to N values. The VectorArithmetic requires definition of magnitudeSquared property and scale method plus implementation of addition and subtraction operations from AdditiveArithmetic.
Our type is implemented as a standard Euclidean vector: addition, subtraction, and scale of these vectors are being done per-value and the magnitude of the vector is computed as a sum of all squared values. In case you need to refresh this part of high-school math, check this wikipedia article.
The whole implementation of AnimatableVector:
With AnimatableVector you have now complete freedom in building animated shapes and views. One of the handiest use cases is probably the creation of various animated charts, so let me demonstrate it here as well.
I present to you my implementation of AnimatableGraph that plots the values either as a chart line or whole area below it. As you can see, the chart values (here named as controlPoints) are stored and passed as AnimatableVector.
Now, you can style this shape by setting fill and stroke properties and present eye-catching charts that can animate whenever its values are altered:
Now try to play with the AnimationVector by yourself and as a challenge implement morphable shapes like this one below:
Do not hesitate to share your solution or ask for help, I will gladly assist you.
I am looking forward to see your output!
Did you enjoy this article? Do you have anything to add?
Feel free to comment or criticize so the next one is even better. Or share it with other SwiftUI adopters ;)