
Create a complex animation like this using Swift.
With more than 1.4 million apps in the iOS App Store today, it’s a real challenge to make your app stand out. You have a very small window of opportunity to capture the attention of your users before your app ends up in the big black hole of obscurity.
There’s no better place to start wowing your users than at the loading screen of your app, where you can add a delightful animation that serves as a precursor to your on-boarding or authentication workflow.
In this tutorial you will learn how to make such an animation. You’ll learn how to build it up piece-by-piece, utilising advanced techniques to create a fluid and captivating animation.
Getting Started
Download the starter project for this tutorial here, save it to a convenient location and open it in Xcode.
Open HolderView.swift. In this UIView
subclass, you will add and animate the following sublayers (found in the Layers subgroup) as shown in the animation above:
- OvalLayer.swift: This is the first layer, which expands from zero size and then wobbles for a short period of time.
- TriangleLayer.swift: This next layer appears while the
OvalLayer
is wobbling. When this view rotates,OvalLayer
contracts back to zero size leaving just theTriangleLayer
visible. - RectangleLayer.swift: This layer serves as a visual container of sorts for the
TriangleLayer
. - ArcLayer.swift: This layer fills the
RectangleLayer
with an animation effect that’s very similar to a glass being filled with water.
Open OvalLayer.swift; the starter project already contains the code to initialize this layer and all the Bezier paths you’ll use in your animations. You’ll see that expand()
, wobble()
and contract()
are all empty; you’ll populate those methods as you work through the tutorial. All the other *Layer files are structured in a similar fashion.
Finally, open ViewController.swift and take a look at addHolderView()
; this method adds an instance of HolderView
as a subview to the center of the view controller’s view. This view will house all the animations. The view controller just needs to put it on the screen, and the view will take care of the actual animation code.
The animateLabel()
function is a delegate callback provided by the HolderView
class that you will fill in as you complete the animation sequence. addButton()
simply adds a button to the view so that you can tap and restart the animation.
Build and run your app; you should see an empty white screen. An empty canvas — the perfect thing on which to start creating your new animations! :]
By the end of this tutorial, your app will look like this:
So without further ado, let’s get started!
Adding The Oval
The animation starts with a red oval that expands into view from the centre of the screen and then wobbles around a bit.
Open HolderView.swift and declare the following constant near the top of the HolderView
class:
let ovalLayer = OvalLayer() |
Now add the following function to the bottom of the class:
func addOval() { layer.addSublayer(ovalLayer) ovalLayer.expand() } |
This first adds the OvalLayer
instance you created above as a sublayer to the view’s layer, then calls expand()
, which is one of the stubbed-out functions you need to fill in.
Go to OvalLayer.swift and add the following code to expand()
:
func expand() { var expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path") expandAnimation.fromValue = ovalPathSmall.CGPath expandAnimation.toValue = ovalPathLarge.CGPath expandAnimation.duration = animationDuration expandAnimation.fillMode = kCAFillModeForwards expandAnimation.removedOnCompletion = false addAnimation(expandAnimation, forKey: nil) } |
This function creates an instance of CABasicAnimation
that changes the oval’s path from ovalPathSmall
to ovalPathLarge
. The starter project provides both of these Bezier paths for you. Setting removedOnCompletion
to false
and fillMode
to KCAFillModeForwards
on the animation lets the oval retain its new path once the animation has finished.
Finally, open ViewController.swift and add the following line to addHolderView()
just below view.addSubview(holderView)
:
holderView.addOval() |
This calls addOval
to kickstart the animation after it has been added to the view controller’s view.
Build and run your app; your animation should now look like this:
Wobbling The Oval
With your oval now expanding into view, the next step is to put some bounce in its step and make it wobble.
Open HolderView.swift and add the following function to the bottom of the class:
func wobbleOval() { ovalLayer.wobble() } |
This calls the stubbed-out method wobble()
in OvalLayer
.
Now open OvalLayer.swift and add the following code to wobble()
:
func wobble() { // 1 var wobbleAnimation1: CABasicAnimation = CABasicAnimation(keyPath: "path") wobbleAnimation1.fromValue = ovalPathLarge.CGPath wobbleAnimation1.toValue = ovalPathSquishVertical.CGPath wobbleAnimation1.beginTime = 0.0 wobbleAnimation1.duration = animationDuration // 2 var wobbleAnimation2: CABasicAnimation = CABasicAnimation(keyPath: "path") wobbleAnimation2.fromValue = ovalPathSquishVertical.CGPath wobbleAnimation2.toValue = ovalPathSquishHorizontal.CGPath wobbleAnimation2.beginTime = wobbleAnimation1.beginTime + wobbleAnimation1.duration wobbleAnimation2.duration = animationDuration // 3 var wobbleAnimation3: CABasicAnimation = CABasicAnimation(keyPath: "path") wobbleAnimation3.fromValue = ovalPathSquishHorizontal.CGPath wobbleAnimation3.toValue = ovalPathSquishVertical.CGPath wobbleAnimation3.beginTime = wobbleAnimation2.beginTime + wobbleAnimation2.duration wobbleAnimation3.duration = animationDuration // 4 var wobbleAnimation4: CABasicAnimation = CABasicAnimation(keyPath: "path") wobbleAnimation4.fromValue = ovalPathSquishVertical.CGPath wobbleAnimation4.toValue = ovalPathLarge.CGPath wobbleAnimation4.beginTime = wobbleAnimation3.beginTime + wobbleAnimation3.duration wobbleAnimation4.duration = animationDuration // 5 var wobbleAnimationGroup: CAAnimationGroup = CAAnimationGroup() wobbleAnimationGroup.animations = [wobbleAnimation1, wobbleAnimation2, wobbleAnimation3, wobbleAnimation4] wobbleAnimationGroup.duration = wobbleAnimation4.beginTime + wobbleAnimation4.duration wobbleAnimationGroup.repeatCount = 2 addAnimation(wobbleAnimationGroup, forKey: nil) } |
That’s a lot of code, but it breaks down nicely. Here’s what’s going on:
- Animate from the large path down to being squished vertically.
- Change from a vertical squish to squished both horizontally and vertically.
- Swap back to vertical squish.
- Finish the animation, ending back at the large path.
- Combine all of your animations into a
CAAnimationGroup
and add this group animation to yourOvalLayout
.
The beginTime
of each subsequent animation is the sum of the beginTime
of the previous animation and its duration
. You repeat the animation group twice to give the wobble a slightly elongated feel.
Even though you now have all the code required to produce the wobble animation, you aren’t calling your new animation yet.
Go back to HolderView.swift and add the following line to the end of addOval()
:
NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: "wobbleOval", userInfo: nil, repeats: false) |
Here you create a timer that calls wobbleOval()
right after the OvalLayer
has finished expanding.
Build and run your app; check out your new animation:
It’s very subtle, but that’s an important factor of a truly delightful animation. You don’t need things to be flying all over the screen!
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!
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:
Filling In The Container
With your container now in place, the next phase of the animation is to fill it up. The effect you’re looking for is that of water filling up a glass. This is a great visual effect and sets things up for a big…splash! :]
Open HolderView.swift and add the following constant just below the two RectangleLayer
properties:
let arcLayer = ArcLayer() |
Now add the following code to the end of drawBlueAnimatedRectangle()
:
NSTimer.scheduledTimerWithTimeInterval(0.40, target: self, selector: "drawArc", userInfo: nil, repeats: false) |
This creates a timer to call drawArc()
once the blue RectangleLayer
finishes drawing.
Add the following function to the end of the class:
func drawArc() { layer.addSublayer(arcLayer) arcLayer.animate() } |
This adds the instance of ArcLayer
created above to the HolderView
‘s layer before you animate in the fill.
Open ArcLayer.swift and add the following code to animate()
:
func animate() { var arcAnimationPre: CABasicAnimation = CABasicAnimation(keyPath: "path") arcAnimationPre.fromValue = arcPathPre.CGPath arcAnimationPre.toValue = arcPathStarting.CGPath arcAnimationPre.beginTime = 0.0 arcAnimationPre.duration = animationDuration var arcAnimationLow: CABasicAnimation = CABasicAnimation(keyPath: "path") arcAnimationLow.fromValue = arcPathStarting.CGPath arcAnimationLow.toValue = arcPathLow.CGPath arcAnimationLow.beginTime = arcAnimationPre.beginTime + arcAnimationPre.duration arcAnimationLow.duration = animationDuration var arcAnimationMid: CABasicAnimation = CABasicAnimation(keyPath: "path") arcAnimationMid.fromValue = arcPathLow.CGPath arcAnimationMid.toValue = arcPathMid.CGPath arcAnimationMid.beginTime = arcAnimationLow.beginTime + arcAnimationLow.duration arcAnimationMid.duration = animationDuration var arcAnimationHigh: CABasicAnimation = CABasicAnimation(keyPath: "path") arcAnimationHigh.fromValue = arcPathMid.CGPath arcAnimationHigh.toValue = arcPathHigh.CGPath arcAnimationHigh.beginTime = arcAnimationMid.beginTime + arcAnimationMid.duration arcAnimationHigh.duration = animationDuration var arcAnimationComplete: CABasicAnimation = CABasicAnimation(keyPath: "path") arcAnimationComplete.fromValue = arcPathHigh.CGPath arcAnimationComplete.toValue = arcPathComplete.CGPath arcAnimationComplete.beginTime = arcAnimationHigh.beginTime + arcAnimationHigh.duration arcAnimationComplete.duration = animationDuration var arcAnimationGroup: CAAnimationGroup = CAAnimationGroup() arcAnimationGroup.animations = [arcAnimationPre, arcAnimationLow, arcAnimationMid, arcAnimationHigh, arcAnimationComplete] arcAnimationGroup.duration = arcAnimationComplete.beginTime + arcAnimationComplete.duration arcAnimationGroup.fillMode = kCAFillModeForwards arcAnimationGroup.removedOnCompletion = false addAnimation(arcAnimationGroup, forKey: nil) } |
This animation is very similar to the earlier wobble animation; you create a CAAnimationGroup
that contains five instances of a path-based CABasicAnimation
. Each path has a slightly different arc with increasing height and is part of the starter project. Finally, you apply the CAAnimationGroup
to the layer and instruct it to not be removed on completion so it will retain its state when the animation has finished.
Build and run your app to watch the magic unfold!
Completing The Animation
All that’s left to do is expand the blue HolderView
to fill in the entire screen and add a UILabel
to the view to serve as the logo.
Open HolderView.swift and add the following code to the end of drawArc()
:
NSTimer.scheduledTimerWithTimeInterval(0.90, target: self, selector: "expandView", userInfo: nil, repeats: false) |
This creates a timer that calls expandView()
after the ArcLayer
fills up the container.
Now, add the following function to the bottom of the same class:
func expandView() { // 1 backgroundColor = Colors.blue // 2 frame = CGRectMake(frame.origin.x - blueRectangleLayer.lineWidth, frame.origin.y - blueRectangleLayer.lineWidth, frame.size.width + blueRectangleLayer.lineWidth * 2, frame.size.height + blueRectangleLayer.lineWidth * 2) // 3 layer.sublayers = nil // 4 UIView.animateWithDuration(0.3, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { self.frame = self.parentFrame }, completion: { finished in self.addLabel() }) } |
Here’s what that method does:
- The background of the holder view is set to blue, to match the color you filled the rectangle with.
- The frame is expanded to account for the
RectangleLayer
‘s stroke width that you added earlier. - All sublayers are removed. Now there are no oval, no triangle and no rectangle layers.
- An animation is added to expand the
HolderView
to fill the screen. Once that animation’s done, you calladdLabel()
.
Add the following function to the bottom of the class:
func addLabel() { delegate?.animateLabel() } |
This simply calls the view’s delegate function to animate the label.
Now open ViewController.swift and add the following code to animateLabel()
:
func animateLabel() { // 1 holderView.removeFromSuperview() view.backgroundColor = Colors.blue // 2 var label: UILabel = UILabel(frame: view.frame) label.textColor = Colors.white label.font = UIFont(name: "HelveticaNeue-Thin", size: 170.0) label.textAlignment = NSTextAlignment.Center label.text = "S" label.transform = CGAffineTransformScale(label.transform, 0.25, 0.25) view.addSubview(label) // 3 UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.1, options: UIViewAnimationOptions.CurveEaseInOut, animations: ({ label.transform = CGAffineTransformScale(label.transform, 4.0, 4.0) }), completion: { finished in self.addButton() }) } |
Taking each commented section in turn:
- Remove
HolderView
from the view and set the view’s background color to blue. - Create a
UILabel
with text of ‘S’ to represent the logo, and add it to the view. - Apply a spring animation to the label to scale it in. Once the animation is done, call
addButton()
to add a button to your view, which, when pressed, repeats the animation.
Build and run the application, give yourself a pat on the back and take a moment to enjoy what you’ve built! :]
Where to Go From Here?
You can download the final completed project here.
This tutorial covered quite a few different animation techniques that, when stacked together, create a rather complex loading animation that really makes your app shine on first run.
From here, feel free to play around with different timings and shapes to see what cool animations you can come up with.
If you want to take your new found animation skills to the next level, then I suggest you check out our book, iOS Animations by Tutorials.
I hope that you had a ton of fun going through this tutorial, and if you have any questions or comments, please join the forum discussion below!
The post How to Create a Complex Loading Animation in Swift appeared first on Ray Wenderlich.