How To Implement A Circular Image Loader Animation with CAShapeLayer

Learn how to add an eye-catching circular loading animation to your iOS apps using CAShapeLayer and Swift in this tutorial. By Michael Katz.

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

Creating the Reveal Animation

The reveal phase gradually displays the image in a window in the shape of an expanding circular ring. If you’ve read this tutorial on creating a Ping-style view controller animation, you'll know this is a perfect use-case of the mask property of a CALayer.

Open CircularLoaderView.swift and add the following method:

func reveal() {
  // 1
  backgroundColor = .clear
  progress = 1
  // 2
  circlePathLayer.removeAnimation(forKey: "strokeEnd")
  // 3
  circlePathLayer.removeFromSuperlayer()
  superview?.layer.mask = circlePathLayer
}

This is an important method to understand, so let's go over this section by section:

  1. You clear the view’s background color so the image behind the view isn’t hidden anymore, and you set progress to 1.
  2. You remove any pending implicit animations for the strokeEnd property, which may have otherwise interfered with the reveal animation. For more about implicit animations, check out iOS Animations by Tutorials.
  3. You remove circlePathLayer from its superLayer and assign it instead to the superView’s layer mask, so the image is visible through the circular mask "hole". This lets you reuse the existing layer and avoid duplicating code.

Now you need to call reveal() from somewhere. Replace the Reveal image here comment in CustomImageView.swift with the following:

if let error = error {
  print(error)
}
self?.progressIndicatorView.reveal()

Build and run. Once the image downloads you'll see it partially revealed through a small ring:

CAShapeLayer tutorial

You can see your image in the background — but just barely! :]

Expanding Rings

Your next step is to expand this ring both inwards and outwards. You could do this with two separate, concentric UIBezierPath, but you can do it in a more efficient manner with just a single Bezier path.

How? You simply increase the circle’s radius to expand outward by changing the path property, while simultaneously increasing the line's width to make the ring thicker and expand inward by changing the lineWidth property. Eventually, both values grow enough to reveal the entire image underneath.

Open CircularLoaderView.swift and add the following code to the end of reveal():

// 1
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let finalRadius = sqrt((center.x*center.x) + (center.y*center.y))
let radiusInset = finalRadius - circleRadius
let outerRect = circleFrame().insetBy(dx: -radiusInset, dy: -radiusInset)
let toPath = UIBezierPath(ovalIn: outerRect).cgPath

// 2
let fromPath = circlePathLayer.path
let fromLineWidth = circlePathLayer.lineWidth

// 3
CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
circlePathLayer.lineWidth = 2*finalRadius
circlePathLayer.path = toPath
CATransaction.commit()

// 4
let lineWidthAnimation = CABasicAnimation(keyPath: "lineWidth")
lineWidthAnimation.fromValue = fromLineWidth
lineWidthAnimation.toValue = 2*finalRadius
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.fromValue = fromPath
pathAnimation.toValue = toPath

// 5
let groupAnimation = CAAnimationGroup()
groupAnimation.duration = 1
groupAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
groupAnimation.animations = [pathAnimation, lineWidthAnimation]
circlePathLayer.add(groupAnimation, forKey: "strokeWidth")

This might look like a lot of code, but what you're doing here is fairly simple:

  1. You determine the radius of the circle that can fully circumscribe the image view and use it to calculate the CGRect that would fully bound this circle. toPath represents the final shape of the CAShapeLayer mask like so:
    CAShapeLayer tutorial
  2. You set the initial values of lineWidth and path to match the current values of the layer.
  3. You set lineWidth and path to their final values. This prevents them from jumping back to their original values when the animation completes. By wrapping this changes in a CATransaction with kCATransactionDisableActions set to true you disable the layer’s implicit animations.
  4. You create two instances of CABasicAnimation: one for path and the other for lineWidth. lineWidth has to increase twice as fast as the radius increases in order for the circle to expand inward as well as outward.
  5. You add both animations to a CAAnimationGroup, and add the animation group to the layer.

Build and run your project. You’ll see the reveal animation kick-off once the image finishes downloading:

CAShapeLayer tutorial

Notice a portion of the circle remains on the screen once the reveal animation is done. To fix this, add the following extension to the end of CircularLoaderView.swift implementing animationDidStop(_:finished:):

extension CircularLoaderView: CAAnimationDelegate {
  func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    superview?.layer.mask = nil
  }
}

This code removes the mask on the super layer, which removes the circle entirely.

Finally, at the bottom of reveal(), just above the line circlePathLayer.add(groupAnimation, forKey: "strokeWidth") add the following line:

groupAnimation.delegate = self

This assigns the delegate so the animationDidStop(_:finished:) gets called.

Build and run your project. Now you’ll see the full effect of your animation:

CAShapeLayer tutorial

Congratulations, you've finished creating the circular image loading animation!

Where to Go From Here?

You can download the completed project here.

From here, you can further tweak the timing, curves and colors of the animation to suit your needs and personal design aesthetic. One possible improvement is to use kCALineCapRound for the shape layer's lineCap property to round off the ends of the circular progress indicator. See what improvements you can come up with on your own!

If you enjoyed this CAShapeLayer tutorial and would like to learn how to create more animations like these, check out Marin Todorov's book iOS Animations by Tutorials, which starts with basic view animations and moves all the way to layer animations, animating constraints, view controller transitions, and more.

If you have any questions or comments about the CAShapeLayer tutorial, please join the discussion below. I'd also love to see ways in which you've incorporated this cool animation in your app!