Chapters

Hide chapters

iOS Animations by Tutorials

Sixth Edition · iOS 13 · Swift 5.1 · Xcode 11

Section IV: Layer Animations

Section 4: 9 chapters
Show chapters Hide chapters

17. Stroke & Path Animations
Written by Marin Todorov

In this chapter you’ll learn about stroke and path animations as you add a cool pull-to-refresh animation to your existing Pack List project that entertains the user while the app pretends to fetch new data from the Internet:

Along the way, you’ll learn how to animate the drawing of shapes, and as a bonus, you’ll look at a special kind of keyframe animation that you can use to move an object along any arbitrary path.

Creating interactive stroke animations

Open the starter project for this chapter, then build and run it to see how the UI looks:

There’s existing code in ViewController.swift to populate the table for you with a number of vacation items. Pull down the table and you’ll see a refresh view appear at the top of the screen:

The refresh view stays visible for four seconds, then retracts. Your job here is to add an amusing animation to entertain users while they wait.

The refresh view already contains all the code for the pulling and releasing actions; you just need to worry about adding the animations.

Note: The pull down to refresh code is based on one of our video tutorials. If you would like to know more about how it works check out the Swift Scroll View School video series at the following link: https://videos.raywenderlich.com.

The first step in building your animation is to create a circle shape. Open RefreshView.swift and add the following code to init(frame:scrollView:):

ovalShapeLayer.strokeColor = UIColor.white.cgColor
ovalShapeLayer.fillColor = UIColor.clear.cgColor
ovalShapeLayer.lineWidth = 4.0
ovalShapeLayer.lineDashPattern = [2, 3]

let refreshRadius = frame.size.height/2 * 0.8

ovalShapeLayer.path = UIBezierPath(ovalIn: CGRect(
  x: frame.size.width/2 - refreshRadius,
  y: frame.size.height/2 - refreshRadius,
  width: 2 * refreshRadius,
  height: 2 * refreshRadius)
).cgPath

layer.addSublayer(ovalShapeLayer)

ovalShapeLayer is a property on RefreshView of type CAShapeLayer. You’re already quite familiar with shape layers; here you simply set the stroke and fill colors and set the circle diameter to be 80% of the view height, which ensures a comfortable margin around the shape.

There’s one property in the code above that you haven’t encountered yet: lineDashPattern. This property lets you set a dash pattern for the shape stroke; you simply you provide an array with the length of the dash and the length of the gap in pixels.

Build and run your project to see how the circle looks:

That looks really nice — and it was easy to create. This will serve you well as a circular progress bar.

In RefreshView, redrawFromProgress() is called whenever the user scrolls via scrollViewDidScroll(_ scrollView:); this makes it a convenient place to update the visuals of the progress bar.

Add the following code to redrawFromProgress():

ovalShapeLayer.strokeEnd = progress

As progress increases from 0.0 to 1.0, the stroke end of the shape moves forward towards the starting point of the stroke.

This is how the shape looks when progress is 0.25:

Here it is halfway around, at 0.5:

Build and run your project; drag the table view up and down to see the stroke length change.

Now you’ll add a cool-looking airplane to the refresh control.

Scroll back to init(frame:scrollView:) and add the following code to the bottom of the initializer:

let airplaneImage = UIImage(named: "airplane.png")!
airplaneLayer.contents = airplaneImage.cgImage
airplaneLayer.bounds = CGRect(x: 0.0, y: 0.0,   
  width: airplaneImage.size.width,   
  height: airplaneImage.size.height)

airplaneLayer.position = CGPoint(  
  x: frame.size.width/2 + frame.size.height/2 * 0.8,   
  y: frame.size.height/2)

layer.addSublayer(airplaneLayer)

The code should look familiar. You’ve already done this a few times in previous chapters. You simply load airplane.png and assign it as the contents of a layer on the screen, then position the airplane layer at the location where the circle starts drawing.

Build and run your project to see the airplane appear when you pull the table view down:

The plane should fade in as the user pulls the table down.

Add the following code to init(frame:scrollView:):

airplaneLayer.opacity = 0.0

This makes the airplane completely transparent to start.

Now add the following code to redrawFromProgress() to progressively change the opacity of the airplane layer as the user pulls down:

airplaneLayer.opacity = Float(progress)

opacity is of type Float so you need to convert progress from a CGFloat.

Build and run your project; pull down the table and you should see the airplane appear gradually:

This wraps up the interactive animation part of this chapter. The next section walks you through animating a progress indicator to keep the users engaged while they wait for the faux refreshed data.

Animating both stroke ends

In this section, you’ll animate both the strokeStart and strokeEnd properties to make the shape “run around”.

You’ll start the animation when the app starts to fetch the faux data. Add the following code to the end of beginRefreshing():

let strokeStartAnimation = CABasicAnimation(
  keyPath: "strokeStart")
strokeStartAnimation.fromValue = -0.5
strokeStartAnimation.toValue = 1.0
  
let strokeEndAnimation = CABasicAnimation(
  keyPath: "strokeEnd")
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0

This code creates two animations: the first one animates strokeStart from -0.5 to 1.0. This is a simple and cheap animation trick; while the value animates from -0.5 to 0.0 nothing happens, as all negative values for these properties simply mean no part of the shape is visible.

This gives the second animation — the one on strokeEnd — a bit of a head start. This draws a small bit of the shape on the screen, until strokeStart catches up with strokeEnd at the end of the animation.

Add the following code the end of beginRefreshing() to run the two animations simultaneously:

let strokeAnimationGroup = CAAnimationGroup()
strokeAnimationGroup.duration = 1.5
strokeAnimationGroup.repeatDuration = 5.0
strokeAnimationGroup.animations = 
  [strokeStartAnimation, strokeEndAnimation]
ovalShapeLayer.add(strokeAnimationGroup, forKey: nil)

In the code above, you create an animation group and repeat the animation five times. This should be long enough to keep the animation running while the refresh view is visible. Then you add both animations to the group and add the group to the progress bar.

Build and run your project; pull and release the table to see your animation in action:

You’ve just created your own custom spinner! Although it looks pretty neat, you can make it even cooler with just a little effort - and some help from layer path animations!

Creating path keyframe animations

You saw how to animate a layer using a keyframe animation and the values property in Chapter 12, “Keyframe Animations and Struct Properties”. to animate a layer along a path you do much the same thing but you assign a CGPath to the animation’s path property instead.

Core Animation will then calculate the intermediate positions of the layer along the CGPath and animate it nicely over the animation’s duration.

Add the following code to beginRefreshing():

let flightAnimation = CAKeyframeAnimation(keyPath: "position")
flightAnimation.path = ovalShapeLayer.path
flightAnimation.calculationMode = .paced

Here you create a CAKeyframeAnimation and, much like before, set the animated property to position. But this time you assign a value to path. In this case you can re-use the circular path of ovalShapeLayer.

Finally you set the animation calculationMode to a paced mode — this will make sure that the layer animates smoothly along the path.

calculationMode is yet another way you can control the timing of your animation. When you set that property to .paced Core Animation animates your layer with a constant pace ignoring any key times you’ve set. This is very useful for producing smooth animations across an arbitrary path.

The CAAnimationCalculationMode structure offers a few more constants. One interesting one is .discrete. This calculation mode makes Core Animation jump from key value to key value without any interpolation. Yes you got it right — Core Animation has a special mode to produce animations that don’t animate anything.

Enough calculation mode fun — back to the task at hand.

Now you need to create a group out of this animation and run it on the airplane layer. You need the group later on as you’ll add a second animation to complement the first.

Add the following code to beginRefreshing():

let flightAnimationGroup = CAAnimationGroup()
flightAnimationGroup.duration = 1.5
flightAnimationGroup.repeatDuration = 5.0
flightAnimationGroup.animations = [flightAnimation]
airplaneLayer.add(flightAnimationGroup, forKey: nil)

In the code above, you create the group and give it the same duration and repeat count as the progress bar stroke animation. You then add flightAnimation as the only item in the group and add the group to the airplane layer.

Run the animation and check out the pilot’s skills:

You’ve never seen an airshow like this one: the airplane performs loops in the sky and stays perfectly vertical! As amusing as this is, you should probably make the plane fly more naturally.

Note: CAKeyframeAnimation has a property called rotationMode of type CAAnimationRotationMode, which when set to .rotateAuto will automatically orient your layer in the direction it’s moving. However, you’ll create this effect manually in this chapter, as it’s an easy task for simple, circular paths.

Insert the following new animation code above the line where you create flightAnimationGroup:

let airplaneOrientationAnimation = CABasicAnimation(keyPath: 
"transform.rotation")
airplaneOrientationAnimation.fromValue = 0
airplaneOrientationAnimation.toValue = 2.0 * .pi

airplaneOrientationAnimation rotates the layer a full 360 degrees — from 0 to — via its transform. In other words, the airplane image will rotate in a full circle around its center, and since you move the airplane layer along the circular path at the same time, the flight will end up looking natural.

Now you just need to add airplaneOrientationAnimation to the animation group.

Add the new animation to the array as shown below:

flightAnimationGroup.animations = [flightAnimation,
  airplaneOrientationAnimation]

Build and run your project and enjoy your completed pull-down-to-refresh control:

If you’d like to experiment with air acrobatics, you can try setting the rotation animation to go from 0 to — good fun!

Keep in mind that you’re not limited to simple paths such as squares or circles. You can feed just about any CGPath to your keyframe animation and send your layer along some extremely complicated paths.

If you’re going to create more complex path animations and want to stay sane, you’d do well to keep in mind the note about rotationMode earlier in this chapter.

Key points

  • You can animate the very process of drawing a shape on screen by animating the strokeStart and strokeEnd properties of a CAShapeLayer.
  • You can animate a layer along a path on screen by using a keyframe animation and animating the layer’s position property with a given CGPath.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.