Home iOS & Swift Tutorials

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.


  • Other, Other, Other

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 the TriangleLayer 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.

Note: If you want to learn more about Bezier paths, then check out our Modern Core Graphics with Swift tutorial series.

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() {

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):


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() {

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, 
  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:

  1. Animate from the large path down to being squished vertically.
  2. Change from a vertical squish to squished both horizontally and vertically.
  3. Swap back to vertical squish.
  4. Finish the animation, ending back at the large path.
  5. Combine all of your animations into a CAAnimationGroup and add this group animation to your OvalLayout.

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
  // 2  
  // Add the code below
  NSTimer.scheduledTimerWithTimeInterval(0.9, target: self, 
                                         selector: "drawAnimatedTriangle", userInfo: nil, 
                                         repeats: false) 

The code above does the following:

  1. This line adds the TriangleLayer instance you initialized earlier as a sublayer to the HolderView‘s layer.
  2. 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 adds drawAnimatedTriangle() after a delay of 0.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() {

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, 
  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

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:

  1. 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.
  2. Apply a CABasicAnimation to rotate the layer 360 degrees, or 2*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.
  3. Call contract() on OvalLayer 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() {
func drawBlueAnimatedRectangle() {

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() {

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

Here’s what that method does:

  1. The background of the holder view is set to blue, to match the color you filled the rectangle with.
  2. The frame is expanded to account for the RectangleLayer‘s stroke width that you added earlier.
  3. All sublayers are removed. Now there are no oval, no triangle and no rectangle layers.
  4. An animation is added to expand the HolderView to fill the screen. Once that animation’s done, you call addLabel().

Add the following function to the bottom of the class:

func addLabel() {

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
  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)

  // 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

Taking each commented section in turn:

  1. Remove HolderView from the view and set the view’s background color to blue.
  2. Create a UILabel with text of ‘S’ to represent the logo, and add it to the view.
  3. 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!




More like this