This is my first ever tutorial of this kind and I appreciate any feedback (including wrong English sentences and unclear explanations). Thanks!
I found this animation in Twitter and was amazed with breathtaking effect of the wave-like expanding and transformation. Here is original GIF:
— Charlie Deck (@bigblueboo) June 9, 2016
Also it wasn’t the same transformation that you can find in Stackoverflow :) What’s a difference? Look:
It looks complex but it’s feasible to make it in Swift with CALayer, CAKeyFramedAnimation and UIBezierPath. Hardest thing here is not the transformation but timing function for animation. I tried to make it as close as possible, but 100% close reproduction isn’t a target of this tutorial.
Code is Swift 3 and written in XCode 8.
First of all, download the base project. It contains all required (empty) files and some helper functions that we will use. Take a brief look before continue.
Let’s start with definition of all functions and properties that will allow us to do experiments with animation easily.
MutableShapeLayer.swift. There are two classes
MutableShapeLayer. First class acts as container and responsible for animation and shapes management.
MutableShapeLayer contains logic for shape animation.
Add new properties at top of MutableShapeView class
shapes will keep list of shapes that app should animate.
step define how thick shapes will be, how big smallest shape is and how big empty space between shapes. It’s important to have it as a parameters because
step need to be changed simultaneously and their values used in several calculations.
Add two functions to
CAShapeLayer doesn’t have initializer that allows to setup frame and position. Personally, I prefer so we need to write function that will help to setup the layer. Also, we need to define UIBezierPath for each stage of the animation.
We need two paths that will represent our shapes. Circle and rectangle. Let’s start with circle definition.
Add this code into MutableShapeLayer() class
Simple. We created oval shape with constant radius that will be returned when requested.
Now, rectangle. We will use UBezierPath with four connected quad curves.
If you’ll render it then it will look like rectangle, without any bend.
Why not to use UIBezierPath(rect:CGRect)? That’s a good question. Our animation transforms circle to rectangle in very specific way. It is like pulling all 4 corners from the inside of a circle and stretching it to rectangle. As a result, we see sharp peaks from the beginning.
If we define path with curves then transform will behave as needed, but If path defined with UIBezierPath(rect:CGRect) then animation will round corner and we will loose this sharp effect on transformation. Here, how it looks like:
UIBezierPath with quad curves
Actually, it’s exactly the same as cornerRadius animation. The only difference is that cornerRadius requires
maskToBound = true, and stroke requires it to be false. So, it might be used to produce different visual effects.
btw. Rect also has weird effect for stroke. Seems like it
Perfect! Now we need to setup the shape. Add new method on top of MutableShapeLayer
Here we moved all properties that styles our shape.
Finally we need to add method that can run animation for MutableShapeLayer.
Add this code below setup() function
What happens here.
CAAnimationinitialized to animate
valuesarray contains data for transformation calculations.
- Timing function is a result of numerous tries in CAMediaTimingFunction playground. It gave pretty close result to original motion in GIF. Robert Böhenke wrote an article that explains how timing functions work, read it on objc.io.
- CAKeyframeAnimation used instead of CABasicAnimation because behavior of the shape isn’t constant and motion distributed isn’t equal on time line (it’s combination of easy-in and fast-out).
- We need to have small delays in the beginning of the animation and end. It possible to implement with help of animation delegate or with keyTimes property of CAKeyframeAnimation. keyTimes much simpler :)
- Animation is infinite and reversing, theoretically it is not necessary to set isRemovedOnCompletion and fillMode, but It’s good to have it in code if you’ll test one animation cycle (from circle to rectangle or vise versa).
- Wave effect achieved with passing different start times for the animation. After couple hours of experiments, I find that delay equal to 1/5 – 1/7 fraction of step is good enough.
Animation starts with some delay from 0 to duration. It starts with circle shape and waits 20% of the duration to start transformation to rectangle. Transformation ends when 60% of animation duration passed. Then animation waits rest of the duration and starts animations in reverse order. Delays in the beginning and in the end of animation needed for better visual effect of the transformation. If animation will start right after transformation then circle will never look like a rectangle with sharp corners.
Okey, we have layer and animation. Now, we need to draw shapes on a container view and start animation.
Add the following code to generateShapes() function
This code initializes all layers for animation. Pay attention to this code
minimalSize + (step * CGFloat(i)). Here value of type
CGFloat added to
CGSize. This possible because operator is overloaded, you can find it in
Now, if you’d render MutableShapeView on screen then you will see image like this.
Navigate to MutableShapeView.animate() function and replace
//Animation comment with the code
We are almost finished. To see animation in the app we need add new subview to ViewController and run animation. Add this code to
AAAAND RUN! I mean build & run.
This is not expected to be like that. Why is it happened?
When animation transforms path the only parameter it may use as anchor is start point of a path. When we defined rectangle start point was in Point(0, 0), but circle’s start point is in Point(shape.width, 0). Animation have to move start point of path we started with to target path and rotate shape during the animation. To show the circle we use stroke (border line) and when path distorted significantly it may has a result of very sharp corners in visibly random positions.
To fix this we need to redefine rectangle shape, so it will start in Point(shape.width, 0) or change starting point of circle shape. But… We cannot move rectangle’s start point to Point(shape.width, 0) because it is the middle of edge and we cannot put circle’s start point to the same position of our rectangle shape (because circle need to be inscribed into rectangle), but… if we will think in polar coordinates then we may rotate starting point of circle’s start point to the same angle where rectangle’s starting point located (not big math, it’s 225º).
Find property MutableShapeLayer.circlePath() and add this code before return
Try to change angle for rotation (e.g. to M_PI_2 + M_PI_4/2.0) to see how different results could be.
Build and run. Now the transformation has no weird stuff at all.
It looks fine, but in the original animation internal and external faces animated differently. We may do this by defining complex polygon for a shape’s path, but it hard to control during animation, so let’s add intermediate shapes same as we have, but colored with white.
We need to modify
generateShapes() in MutableShapeView and
setup in MutableShapeLayer.
Now setup() function for MutableShapeLayer to support color parameter.
We added more shapes. Even numbered are black and odd numbered are white. Also, we inserted each layer at index zero and this places layers in opposite order to
addSublayer. In this order smaller shape will overlap next in line. If we will find correct delays for animation for white and black shapes then it will look like internal and external face of black shape transformed differently. Magic.
animate() function in MutableShapeView class.
Build and run.
Ufff… not bad, right?
What to do next?
- Try to play with keyTimes, colors and shadows.
- Try to draw outer circle as a thin line.
- Try to use different shapes, or add more animations. Results may vary (yes, this is circle to rectangle transformation, but circle defined as 4 quad curves and start/end points calculated in runtime):