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.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

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

BeginningMorph

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:

  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:

morphending

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:

containerDraw

Satraj

Contributors

Satraj

Author

Over 300 content creators. Join our team.