Hello and welcome to another blog post about SwiftUI animations. In the previous post dedicated mainly to AnimatableData
, we have constructed AnimatableVector
that allowed us to create animatable charts. Today we will utilize the same class for morphing shapes.
Shape representation
Shapes in SwiftUI can be constructed as a composition of vector paths and/or shape primitives. If we want to create morphing animation between two shapes, we need to find universal representation first. This can be done either ad-hoc for specific shapes - as we saw in the previous post about custom controls - but it is better to find a more generic one that will allow us to morph any shape to any shape without limitations.
The most common approach is to describe the shape as an approximation of its outline using a finite set of line segments. This means, that we will distribute N
points around the shape outline and when doing the morphing animation, we can easily move these outline points from the position given by shape A to the new position in the shape B. For better understanding, check the following illustration:
Implementation of morphable shape in SwiftUI
So how to implement all of this in SwiftUI? Let me start with a new shape representation based on multiple control points. This is quite easy, the only crucial part is that these control points are being represented using our AnimatableVector
so it is capable to morph using animation.
But hey - the shape points are two-dimensional (having x and y coordinate), while our AnimatableVector
holds only an array of Doubles. What can be done about that? Well, there are two solutions:
- you can implement another custom vector holding array of
CGPoints
and let it conform to theAnimatableData
protocol - or use the same
AnimatableVector
and expect even indexed component to be x coordinate and odd-indexed components to by y coordinate. I choose this solution for the followingMorphableShape
implementation:
Let’s check the implementation using several random points:
Converting standard shape to MorphableShape
All the above code is enough to morph any shape to any shape, but getting the shape representation as a set of control points may be tricky. Well, it is definitively something you cannot put together “by hand”.
Luckily, there is a way how to generate them using SwiftUI Path API. We can sample the shape outline using the trimmedPath()
method, so let me create a simple Path extension, that provides a vector of N control points along any Path:
Now, it can be utilized like this:
Please, note, that with setting the CGRect
size to 1 we are assuring to have the control point coordinates in the range 0...1
which is exactly what our MorphableShape
expects to get.
And that’s it!
Use cases
If you are wondering, what is this all good for, I present here several ideas:
-
morphing of icons (for multi-state buttons)
-
custom transitions and morphing of clip masks
-
creating a pointless music video in SwiftUI 🤪
Final notes
- Described algorithm works best for one-component/compact shapes. It works also for multi-component shapes, but the resulting animation may not be eye-pleasant.
- For large curved or complex shapes, the number of control points needs to be quite high (hundreds at least) to have a smooth result.
- The only tricky thing with this approach is that the shapes may differ in the origin and direction of how they are being constructed. If these parameters are different, the morphing animation would not work as expected and shape may “flip” during interpolation. When creating my custom shapes, I usually start the path at the top-left corner and construct the shape in the clockwise direction to avoid these issues. Nevertheles, the control points could be programatically rearranged so the vector always start around the same orientation - but so far I have not implemented this part:)
- It would be awesome, if we could get Path from SFSymbols….🤞
Did you like this article? What do you want me to focus on next?
Feel free to comment or criticize so the next one is even better. Or share it with other SwiftUI adopters ;)