How to Create a Complex Loading Animation in Swift
Learn how to create a complex loading animation in Swift with this step-by-step tutorial. By Satraj.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Create a Complex Loading Animation in Swift
20 mins
Beginning The Morph
It’s time to get a little fancy! :] You’re going to morph the oval into a triangle. To the user’s eye, this transition should look completely seamless. You’ll use two separate shapes of the same colour to make this work.
Open HolderView.swift and add the following code to the top of HolderView
class, just below the ovalLayer
property you added earlier:
let triangleLayer = TriangleLayer()
This declares a constant instance of TriangleLayer
, just like you did for OvalLayer
.
Now, make wobbleOval()
look like this:
func wobbleOval() {
// 1
layer.addSublayer(triangleLayer) // Add this line
ovalLayer.wobble()
// 2
// Add the code below
NSTimer.scheduledTimerWithTimeInterval(0.9, target: self,
selector: "drawAnimatedTriangle", userInfo: nil,
repeats: false)
}
The code above does the following:
- This line adds the
TriangleLayer
instance you initialized earlier as a sublayer to theHolderView
‘s layer. - Since you know that the wobble animation runs twice for a total duration of
1.8
, the half-way point would be a great place to start the morphing process. You therefore add a timer that addsdrawAnimatedTriangle()
after a delay of0.9
.
Note: Finding the right duration or delay for animations takes some trial and error, and can mean the difference between a good animation and a fantastic one. I encourage you to tinker with your animations to get them looking perfect. It can take some time, but it’s worth it!
Note: Finding the right duration or delay for animations takes some trial and error, and can mean the difference between a good animation and a fantastic one. I encourage you to tinker with your animations to get them looking perfect. It can take some time, but it’s worth it!
Next, add the following function to the bottom of the class:
func drawAnimatedTriangle() {
triangleLayer.animate()
}
This method is called from the timer that you just added to wobbleOval()
. It calls the (currently stubbed out) method in triangleLayer
which causes the triangle to animate.
Now open TriangleLayer.swift and add the following code to animate()
:
func animate() {
var triangleAnimationLeft: CABasicAnimation = CABasicAnimation(keyPath: "path")
triangleAnimationLeft.fromValue = trianglePathSmall.CGPath
triangleAnimationLeft.toValue = trianglePathLeftExtension.CGPath
triangleAnimationLeft.beginTime = 0.0
triangleAnimationLeft.duration = 0.3
var triangleAnimationRight: CABasicAnimation = CABasicAnimation(keyPath: "path")
triangleAnimationRight.fromValue = trianglePathLeftExtension.CGPath
triangleAnimationRight.toValue = trianglePathRightExtension.CGPath
triangleAnimationRight.beginTime = triangleAnimationLeft.beginTime + triangleAnimationLeft.duration
triangleAnimationRight.duration = 0.25
var triangleAnimationTop: CABasicAnimation = CABasicAnimation(keyPath: "path")
triangleAnimationTop.fromValue = trianglePathRightExtension.CGPath
triangleAnimationTop.toValue = trianglePathTopExtension.CGPath
triangleAnimationTop.beginTime = triangleAnimationRight.beginTime + triangleAnimationRight.duration
triangleAnimationTop.duration = 0.20
var triangleAnimationGroup: CAAnimationGroup = CAAnimationGroup()
triangleAnimationGroup.animations = [triangleAnimationLeft, triangleAnimationRight,
triangleAnimationTop]
triangleAnimationGroup.duration = triangleAnimationTop.beginTime + triangleAnimationTop.duration
triangleAnimationGroup.fillMode = kCAFillModeForwards
triangleAnimationGroup.removedOnCompletion = false
addAnimation(triangleAnimationGroup, forKey: nil)
}
This code animates the corners of TriangleLayer
to pop out one-by-one as the OvalLayer
wobbles; the Bezier paths are already defined for each corner as part of the starter project. The left corner goes first, followed by the right and then the top. You do this by creating three instances of a path-based CABasicAnimation
that you add to a CAAnimationGroup
, which, in turn, you add to TriangleLayer
.
Build and run the app to see the current state of the animation; as the oval wobbles, each corner of the triangle begins to appear until all three corners are visible, like so:
Completing The Morph
To complete the morphing process, you’ll rotate HolderView
by 360 degrees while you contract OvalLayer
, leaving just TriangleLayer
alone.
Open HolderView.swift add the following code to the end of drawAnimatedTriangle()
:
NSTimer.scheduledTimerWithTimeInterval(0.9, target: self, selector: "spinAndTransform",
userInfo: nil, repeats: false)
This sets up a timer to fire after the triangle animation has finished. The 0.9s time was once again determined by trial and error.
Now add the following function to the bottom of the class:
func spinAndTransform() {
// 1
layer.anchorPoint = CGPointMake(0.5, 0.6)
// 2
var rotationAnimation: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.toValue = CGFloat(M_PI * 2.0)
rotationAnimation.duration = 0.45
rotationAnimation.removedOnCompletion = true
layer.addAnimation(rotationAnimation, forKey: nil)
// 3
ovalLayer.contract()
}
The timer you created just before adding this code calls this function once the the oval stops wobbling and all corners of the triangle appear. Here’s a look at this function in more detail:
- Update the anchor point of the layer to be slightly below the center of the view. This affords a rotation that appears more natural. This is because the oval and triangle are actually offset from the center of the view, vertically. So if the view was rotated around its center, then the oval and triangle would appear to move vertically.
- Apply a
CABasicAnimation
to rotate the layer 360 degrees, or2*Pi
radians. The rotation is around the z-axis, which is the axis going into and out of the screen, perpendicular to the screen surface. - Call
contract()
onOvalLayer
to perform the animation that reduces the size of the oval until it’s no longer visible.
Now open OvalLayer.swift and add the following code to contract()
:
func contract() {
var contractAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
contractAnimation.fromValue = ovalPathLarge.CGPath
contractAnimation.toValue = ovalPathSmall.CGPath
contractAnimation.duration = animationDuration
contractAnimation.fillMode = kCAFillModeForwards
contractAnimation.removedOnCompletion = false
addAnimation(contractAnimation, forKey: nil)
}
This sets OvalLayer
back to its initial path of ovalPathSmall
by applying a CABasicAnimation
. This is the exact reverse of expand()
, which you called at the start of the animation.
Build and run your app; the triangle is the only thing that should be left on the screen once the animation is done:
Drawing The Container
In this next part, you’re going to animate the drawing of a rectangular container to create an enclosure. To do this, you’ll use the stroke property of RectangleLayer
. You’ll do this twice, using both red and blue as the stroke color.
Open HolderView.swift and declare two RectangularLayer
constants as follows, underneath the triangleLayer
property you added earlier:
let redRectangleLayer = RectangleLayer()
let blueRectangleLayer = RectangleLayer()
Next add the following code to the end of spinAndTransform()
:
NSTimer.scheduledTimerWithTimeInterval(0.45, target: self,
selector: "drawRedAnimatedRectangle",
userInfo: nil, repeats: false)
NSTimer.scheduledTimerWithTimeInterval(0.65, target: self,
selector: "drawBlueAnimatedRectangle",
userInfo: nil, repeats: false)
Here you create two timers that call drawRedAnimatedRectangle()
and drawBlueAnimatedRectangle()
respectively. You draw the red rectangle first, right after the rotation animation is complete. The blue rectangle’s stroke begins as the red rectangle’s stroke draws close to completion.
Add the following two functions to the bottom of the class:
func drawRedAnimatedRectangle() {
layer.addSublayer(redRectangleLayer)
redRectangleLayer.animateStrokeWithColor(Colors.red)
}
func drawBlueAnimatedRectangle() {
layer.addSublayer(blueRectangleLayer)
blueRectangleLayer.animateStrokeWithColor(Colors.blue)
}
Once you add the RectangleLayer
as a sublayer to HolderView
, you call animateStrokeWithColor(color:)
and pass in the appropriate color
to animate the drawing of the border.
Now open RectangleLayer.swift and populate animateStrokeWithColor(color:)
as follows:
func animateStrokeWithColor(color: UIColor) {
strokeColor = color.CGColor
var strokeAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeAnimation.fromValue = 0.0
strokeAnimation.toValue = 1.0
strokeAnimation.duration = 0.4
addAnimation(strokeAnimation, forKey: nil)
}
This draws a stroke
around RectangleLayer
by adding a CABasicAnimation
to it. The strokeEnd
key of CAShapeLayer
indicates how far around the path to stop stroking. By animating this property from 0 to 1, you create the illusion of the path being drawn from start to finish. Animating from 1 to 0 would create the illusion of the entire path being rubbed out.
Build and run your app to see how the two strokes look as they build the container: