Chapters

Hide chapters

iOS Animations by Tutorials

Seventh Edition · iOS 15 · Swift 5.5 · Xcode 13

Section IV: Layer Animations

Section 4: 9 chapters
Show chapters Hide chapters

11. Animation Keys & Delegates
Written by Marin Todorov

One of the tricky parts about UIKit animations and the corresponding closure syntax is that, if you aren’t using some of the new APIs like UIViewPropertyAnimator (which you will look into in later chapters), once you create and run a view animation you can’t pause it, stop it or access it in any way.

With Core Animation, however, you can easily inspect animations that are running on a layer and stop them if you need to. Furthermore, you can even set a delegate object on your animations and react to animation events. In contrast to the completion block you’ve seen in view animations, you can receive delegate callbacks for when an animation begins and ends (or is interrupted).

In this chapter you’ll continue to work with the Bahama Air login project and use animation delegates to make your animations more interactive.

Introducing Animation Delegates

CAAnimation and its subclass CABasicAnimation implement the delegate pattern and let you respond to animation events.

CAAnimationDelegate features two methods that you can implement if you need either or both of them:

func animationDidStart(_ anim: CAAnimation)
func animationDidStop(_ anim: CAAnimation, finished flag: Bool)

You can either open the starter project for this chapter, or carry on with your own project if you’ve completed the previous chapter and its challenges.

Your first task is to make each form element pulse once when it reaches its final position on the screen using animationDidStop().

Open ViewController.swift and add the following code to viewWillAppear(), just before the line where you add the animation to your heading layer:

flyRight.delegate = self

Then add the delegate method to the class in an extension to your view controller:

extension ViewController: CAAnimationDelegate {
  func animationDidStop(_ anim: CAAnimation, 
    finished flag: Bool) {
    print("animation did finish")
  }
}

This simply prints out a line to the console to show that you are, in fact, calling your delegate method.

Build and run your project; you should see the following output in the Xcode console:

Okay, so your delegates are being called — but how do you discern which animation stopped in the code above? Remember, you added the same animation to three different layers! The following section shows you how to manage this with key-value pairs.

Key-value Coding Compliance

The CAAnimation class and its subclasses are written in Objective-C and are key-value coding compliant, which means you can treat them like dictionaries and add new properties to them at run time.

You’ll use this mechanism to assign a name to the flyRight animation so that you can identify it out of the pack of other active animations.

Scroll back to viewWillAppear() and add the following code just after the line where you set flyRight’s delegate:

flyRight.setValue("form", forKey: "name")
flyRight.setValue(heading.layer, forKey: "layer")

In the code above, you create the key name on the flyRight animation and set it to form. Now you can check for this name key from your delegate callbacks to identify the animation as belonging to one of your form controls.

You also assign heading.layer to the layer key so you have a reference back to the layer the animation belongs to.

You can leave the name unchanged, but you should update the layer key each time you add the animation to a layer.

Find the line where you add the animation to the username layer, and add the following just before it:

flyRight.setValue(username.layer, forKey: "layer")

Similarly, find the line where you add the animation to the password layer, and add the following just before it:

flyRight.setValue(password.layer, forKey: "layer")

Now each copy of flyRight carries a reference to the layer on which it runs.

Switching on Key Values

Now that you have the keys set on your animation, you can check for them in the animation delegate methods.

Add the following code to the bottom of animationDidStop:

guard let name = anim.value(forKey: "name") as? String else {
  return
}

if name == "form" {
  //form field found
}

In the code above you use value(forKey:) to get the value of name from the animation and attempt to cast the value to String. The guard is used simply because value(forKey:) returns an optional in the case there is no value assigned for the provided key, or if the downcast fails. The optional binding statement takes care of these checks for you. If you can unwrap the value, then you have a value you can test for. If not, then your code carries on.

Now you have a spot to run some code when the form animation completes. Replace the comments //form field found with the following:

let layer = anim.value(forKey: "layer") as? CALayer
anim.setValue(nil, forKey: "layer")

let pulse = CABasicAnimation(keyPath: "transform.scale")
pulse.fromValue = 1.25
pulse.toValue = 1.0
pulse.duration = 0.25
layer?.add(pulse, forKey: nil)

Once that’s done, you set the value of layer to nil to remove the reference to the original layer.

Note: Remember that value(forKey:) always returns an AnyObject?; therefore you must cast the result to your desired type. Don’t forget that the cast operation can fail, so you must use optionals in the example above to handle error conditions, such as when the name key exists but the layer key does not.

Finally, you create an animation that bumps the scale of the layer by a tiny bit (a factor of 1.25), then animates the layer back to its original size (a factor of 1.0).

Note that you’re using optional chaining here with layer? — that means the add(_:forKey:) call will be skipped if there isn’t a layer stored in the animation. And since you set the layer to nil earlier, this pulse animation will only happen the first time the form field flies in from the right. Build and run your project; you should see that any time a flyRight animation completes, the element will pulse once before it comes to rest:

This gives the animations a nice edge and draws the user’s attention to these controls. That takes care of animations that have already stopped; but how can you work with animations that are still running? That’s where animation keys come in.

Animation Keys

You’ve probably noticed that add(_:key:) has two parameters; so far, you’ve only been using the first to pass in the animation object itself.

The key parameter is a string identifier that lets you access and control the animation after it’s been started.

In this part you’ll create another layer animation, learn how to run more than one animation at a time, and discover how to use animation keys to control running animations. You’re going to add a new animated label to your form that gives some basic instructions to the user, as shown below:

Your new label will animate slowly from right to left. As soon as the user starts to enter their username or password, this label will stop moving and jump directly to its final position. There’s no need to continue the animation once the user knows what to do.

First you’ll need to add some code that sets up the label to animate. Add the following property to the end of the property declarations at the top of ViewController:

let info = UILabel()

This is the label that will be used to display instructions to the user.

Next, add the following code to the bottom of viewDidLoad to set up the various properties of your label:

info.frame = CGRect(x: 0.0, y: loginButton.center.y + 60.0, width: view.frame.size.width, height: 30)
info.backgroundColor = .clear
info.font = UIFont(name: "HelveticaNeue", size: 12.0)
info.textAlignment = .center
info.textColor = .white
info.text = "Tap on a field and enter username and password"
view.insertSubview(info, belowSubview: loginButton)

This adds the label just underneath the login button. When the user taps the login button, it will move down and cover the instructions, since the user won’t need them anymore.

Now you need to slide the instructions in from the right when the app first opens. Find viewDidAppear() and add the following code to the end of that method:

let flyLeft = CABasicAnimation(keyPath: "position.x")
flyLeft.fromValue = info.layer.position.x +     
view.frame.size.width
flyLeft.toValue = info.layer.position.x
flyLeft.duration = 5.0
info.layer.add(flyLeft, forKey: "infoappear")

This animates your label much like your form fields, only your label will slide in from the right side of the screen and fly to the left. Note that you set the animation’s key to infoappear; you’ll use this value to find and then stop the animation when the user starts to enter text in either the username or password field.

Adding a Second Layer Animation

You’ll now add a second animation to the label that will fade in the instructions while the label slides in. Add the following code to the end of viewDidAppear:

let fadeLabelIn = CABasicAnimation(keyPath: "opacity")
fadeLabelIn.fromValue = 0.2
fadeLabelIn.toValue = 1.0
fadeLabelIn.duration = 4.5
info.layer.add(fadeLabelIn, forKey: "fadein")

The code fades in the label from a barely visible opacity of 0.2 to a fully visible opacity of 1.0. The above animation runs on a different property — opacity — than in the previous animation — position — and has an entirely different duration. This shows you it’s possible to run multiple animations independently of each other on the same layer.

Note: If you need to synchronize animations on a layer, then you’ll need to use animation groups to achieve this. This technique is covered in the next chapter.

Run the project to see the combined animations in action:

So far, so good. Now comes the tricky part: Cancelling the animation if the user starts to enter their username or password.

Identifying Running Animations

You’ll need to know when the user taps on either the username or password field. Add the following extension to the very bottom of ViewController.swift:

extension ViewController: UITextFieldDelegate {
  func textFieldDidBeginEditing(_ textField: UITextField) {
    guard let runningAnimations = info.layer.animationKeys() else {
      return
    }
    print(runningAnimations)
  }
}

This will add a class extension to say ViewController conforms to the UITextFieldDelegate protocol. Separating protocols into their own extensions like this helps keep the file organized.

The one method you need to implement, textFieldDidBeginEditing, will be called when the user starts editing a text field. For now, you’ll just print out a list of all the active animations running on the label.

Next, add the following code to the bottom of viewDidAppear:

username.delegate = self
password.delegate = self

This will make the class a delegate of both text fields.

Build and run your project; tap either the username or password field before the label animation completes and you should see the following output in your Xcode console:

Success! Once you know which animations are running, you can do several things with them:

  1. Call removeAllAnimations() on the layer to stop all running animations or removeAnimation(forKey:) to remove just one.

  2. Enumerate over the list of animations returned by animationKeys(). The animation objects are immutable, so you can’t modify the animation while they’re in progress.

  3. Fetch an animation by key using animationForKey(_:). As before, the returned animation object will be immutable.

To finish your “stop animation” effect, you’ll cancel the animation that moves the label, but leave the fade animation running as it would look weird for the opacity of the label to jump directly to 1.0.

Add the following code to textFieldDidBeginEditing:

info.layer.removeAnimation(forKey: "infoappear")

This removes the slide-in animation and causes the info label to jump straight to the center of the screen — without affecting the fade-in animation. Build and run your project one final time; tap either of the text fields and you’ll see the instructions jump straight to the center of the screen and continue to fade in:

That’s a wrap! You’ve learned about delegate methods, accessing animations by keys, and created your first composite layer animation as an added bonus! Why not test your knowledge with the following challenge?

Key Points

  • You can set a delegate for your layer animations and be notified whenever the animations starts or finishes.
  • Animations are key-value coding compliant so you can attach arbitrary pieces of data to give you more information about the animation’s context.
  • The add(_, forKey:) API allows you to optionally specify a forKey string parameter to give the particular animation a name.

Challenges

Challenge 1

In this challenge, you’ll strengthen your knowledge of animation delegates and key-value coding by replacing the existing cloud animation with layer animations.

Replace the animate(cloud: UIImageView) method with the following:

  func animateCloud(layer: CALayer) {

    //1
    let cloudSpeed = 60.0 / Double(view.layer.frame.size.width)
    let duration: TimeInterval = Double(
      view.layer.frame.size.width - layer.frame.origin.x) 
      * cloudSpeed
    
    //2
    let cloudMove = CABasicAnimation(keyPath: "position.x")
    cloudMove.duration = duration
    cloudMove.toValue = self.view.bounds.width +
      layer.bounds.width / 2
    cloudMove.delegate = self
    cloudMove.setValue("cloud", forKey: "name")
    cloudMove.setValue(layer, forKey: "layer")
    layer.add(cloudMove, forKey: nil)
  }

This method has a similar implementation to the one that animates the clouds using UIKit. In fact, the first few lines of part //1 above are almost identical to the UIKit implementation.

In the second part of the above method, you create an instance of CABasicAnimation for the layer movement and set a name and layer on the animation.

Next, replace the calls in viewDidAppear() to animateCloud() with the following:

animateCloud(layer: cloud1.layer)
animateCloud(layer: cloud2.layer)
animateCloud(layer: cloud3.layer)
animateCloud(layer: cloud4.layer)

Your task now is to complete the animation by implementing the delegate code to reset each of the clouds.

You already know how to do this through animationDidStop(_: finished:). Just follow these key points:

  • Check if the animation name is "cloud".
  • If so, fetch the animation’s layer from the "layer" key.
  • Set the layer’s position.x to -layer.bounds.width/2.
  • Wait for half a second (use the delay(seconds:) function) and call self.animateCloud(), passing in the current layer.

Build and run your project; check that the clouds reappear once they’ve left the screen:

Congratulations — you’ve learned the basic layer animation techniques and are ready to move on to creating more complex animations. You’ll learn about animation groups next in Chapter 12, “Groups & Advanced Timing.”

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.