10.
Getting Started With Layer Animations
Written by Marin Todorov
Layer animations work much like view animations; you simply animate a property between a start and an end value over a defined period of time and let Core Animation take care of the rendering in between.
However, layers have a bigger number of animatable properties than views; this gives you a lot of choice and flexibility when it comes to designing your effects; many specialized CALayer subclasses add other properties that you can use in your animations.
This chapter will introduce you to the basics of CALayer and Core Animation. You’ll get a feel for working with animations in layers; you’ll learn how to move layers around, fade them in and out and create animations comparable to the ones you created using UIKit.
Animatable Properties
Some of the animatable properties in CALayer correspond directly to the view properties you worked with in previous chapters, such as frame, position and opacity. You’ll see both the familiar and the new animatable properties used in layer animation in this chapter. You’ll re-create some of the earlier view animations but with layers, so you can draw the parallels and see for yourself where the similarities end — and where the new possibilities begin.
Position and Size
Animating the position, size, or transform of a layer equally affects any view contained within that layer, just as if you had directly animated the view itself.
-
bounds: modify this to animate the bounding frame of the layer. -
position: modify this to animate the position of the layer within its parent layer. You can animateposition.xorposition.yseparately if you want to control movement on only one axis. -
transform: modify this to move, scale, and rotate the layer. You can even animate layers in 3D space, which you can’t do with views alone. You’ll learn about 3D layer transforms in Section VII, “3D Animations”.
Border
You can easily animate a layer’s border to change its color, width and corner radius:
-
borderColor: modify this to change the border tint. -
borderWidth: modify this to grow or shrink the width of the border. -
cornerRadius: modify this to change the radius of the layer’s rounded corners.
Shadow
You can animate all aspects of the layer’s shadow:
-
shadowOffset: Modify this to make the shadow appear closer to or further away from the layer. -
shadowOpacity: Modify this to make the shadow fade in or out. -
shadowPath: Modify this to change the shape of the layer’s shadow. You can create different 3D effects to make your layer look like it’s floating with different shadow shapes and positions. -
shadowRadius: Modify this to control the blur of the shadow; this is especially useful when simulating movement of the view towards or away from the surface where the shadow is cast.
Contents
Finally, there are a few properties that control how the layer’s contents are rendered:
-
contents: Modify this to assign raw TIFF or PNG data as the layer contents. -
mask: Modify this to establish the shape or image you’ll use to mask the visible contents of the layer; you’ll use this property to create some very cool effects in Chapter 15, “Shapes & Masks”. -
opacity: Modify this to animate the transparency of the layer contents.
Keep in mind that this is only a partial list of properties you can animate; subclasses of CALayer usually have other properties that you can animate as well.
The properties listed above are enough to get you started; it’s time to get to work on your first animation with layer properties.
Your First Layer Animation
You’ll begin with the completed Bahama Air login screen project from the end of Chapter 5, “Transitions”. As always, you can build on your previous work or open the starter project included with this chapter.
Build and run your project to see the familiar sight of the Bahama Air login screen:
Your job is to remove the existing view animations and replace them one-by-one with layer-based animations.
Open ViewController.swift and find viewWillAppear().
Remove the line below that moves the heading out of the screen bounds:
heading.center.x -= view.bounds.width
There’s no need to perform this action anymore since you can specify both the start and end values in your layer animation.
Next, scroll down to viewDidAppear() and remove the animation call that moves heading as shown below:
UIView.animate(withDuration: 0.5) {
self.heading.center.x += self.view.bounds.width
}
Build and run your project; watch the animation screen and check that the form title is no longer animated:
Now that you’ve stripped out the old view animation code, it’s time to add some layer animations! Find viewWillAppear() and add the following code at the top of the method, underneath the call to super:
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width / 2
flyRight.toValue = view.bounds.size.width / 2
flyRight.duration = 0.5
Animation objects in Core Animation are simply data models; you create an instance of the model and set its data properties accordingly.
An instance of CABasicAnimation describes a potential layer animation: one that you might choose to run now, at a later time, or not at all. Since the animation isn’t bound to a specific layer, you can re-use the animation on other layers and each layer will run a copy of the animation independently.
In an animation model you can specify the property to animate as the keypath argument; that’s convenient, as you’ll always be animating something in the layer.
Here, you’re animating only the x component of the position. Core Animation conveniently exposes the individual members of position, bounds, and transform so you can animate them each separately.
Next, you set the fromValue and toValue for the property you’ve specified on keypath. In this case, you want it to start offscreen to the left and end up in the center of the screen.
Finally, the concept of the animation duration hasn’t changed; here you set the duration to 0.5 seconds. Now that your animation is all set up, you can add it to a layer in your app and see how it looks. Add the following line below the code you just added to add your animation to your title layer:
heading.layer.add(flyRight, forKey: nil)
add(_:forKey:) makes a copy of the animation object and tells Core Animation to run it on the layer. The key argument is for your use only; it lets you identify the animation later on if you need to change or stop the animation.
Build and run your project; you’ll see the form title move to the center of the screen as shown below:
As expected, the layer — and its contained view — animate smoothly into position. This shows you how closely a view and its backing layer are bound.
Note: Animating structs such as
CGRectorCATransform3Disn’t as straightforward as it is with object values, like you did above. You’ll see how to animate structs in Chapter 14, “Layer Keyframe Animations & Struct Properties”.
Now that you have the basics nailed down, things are only going to get more interesting!
More Elaborate Layer Animations
You’ve handled the title layer on your login screen; your next task is to take care of the username field.
Scroll to viewWillAppear() and remove the following line:
username.center.x -= view.bounds.width
Then remove the following view animation from the username field in viewDidAppear():
UIView.animateWithDuration(0.5, delay: 0.3,
usingSpringWithDamping: 0.6, initialSpringVelocity: 0,
animations: {
self.username.center.x += self.view.bounds.width
},
completion: nil
)
Before you rush through and blindly copy and paste the code to create a CABasicAnimation for the username field, consider the following two facts:
-
A
CABasicAnimationobject is just a data model, which is not bound to any particular layer. -
add(_:forKey:)makes a copy of the animation object.
It turns out that you can simply take the animation from your heading layer, adjust the animation a bit if needed and reuse it to animate your username field onto the screen.
Add the following code in the same spot where you deleted the code above in viewWillAppear():
username.layer.add(flyRight, forKey: nil)
Build and run your project to see the resulting effect of re-using your layer animation:
Well, the animation runs, but the title and username field slide onto the screen like those oh-so-unpopular synchronized kill-bots. You’ll need to recreate the timing offset you had in the original animation.
Add the following line just before the line where you add the flyRight animation to your username layer:
flyRight.beginTime = CACurrentMediaTime() + 0.3
The beginTime property of your animation sets the absolute time the animation should start; in this case you get the current time with CACurrentMediaTime() and add to it the desired delay in seconds.
Build and run your app again to see how things look; it appears in the center of the screen, as it was designed in Interface Builder, and starts animating 0.3 seconds later. What gives?
It’s time to learn about another layer animation property called fillMode; below are a few examples of how this property works.
Using fillMode
The fillMode property lets you control the behavior of your animation at the beginning and end of its sequence.
The constant CAMediaTimingFillMode.removed is the default value of fillMode. This starts the animation at the defined beginTime — or instantly, if you haven’t set beginTime — and removes the changes made during the animation when the animation completes:
This is the approach you’ve used so far in this chapter. There are three other options in addition to removed that you can use in your animations:
backwards
CAMediaTimingFillMode.backwards displays the first frame of your animation instantly on the screen, regardless of the actual start time of the animation, and starts the animation at a later time.
forwards
CAMediaTimingFillMode.forwards plays the animation as usual, but retains the final frame of the animation on the screen until you remove the animation:
In addition to setting CAMediaTimingFillMode.forwards, you’ll need to make some other changes to the layer to get the last frame to “stick”. You’ll learn about this a little later in the chapter.
both
CAMediaTimingFillMode.both is a combination of forwards and backwards; as you’d expect, this makes the first frame of the animation appear on the screen immediately and retains the final frame on the screen when the animation is finished:
To fix the issue you discovered earlier, you’ll use both.
Add the following line of code to where you set up flyRight (fromValue, toValue, duration, etc.) and before you add it to a layer:
flyRight.fillMode = .both
Build and run your project; you’ll see that the username field doesn’t appear at first and the animation only starts after a 0.3-second delay. Also, the fields remain in position when the animation completes. You can now animate your password field in a similar fashion. Remove the following line from viewWillAppear():
password.center.x -= view.bounds.width
Then find and remove the following code in viewDidAppear():
UIView.animate(withDuration: 0.5, delay: 0.4, options: .curveEaseOut, animations: {
self.password.center.x += self.view.bounds.width
}, completion: nil)
…and replace it with the following code in viewWillAppear():
flyRight.beginTime = CACurrentMediaTime() + 0.4
password.layer.add(flyRight, forKey: nil)
Build and run your project; you’ll see all three layers flying in, with the password field arriving just a tenth of a second behind the username field:
So far, your animations have happened to end at the exact position where the form elements were originally positioned in Interface Builder. Many times, this won’t be the case. In the next section of this chapter, you’ll discover how to handle the situation where layers end at a different position!
Animations vs. real content
First, you’ll put the text fields off screen at the start of the animation. For testing purposes, add the following code to the start of viewWillAppear():
username.layer.position.x -= view.bounds.width
password.layer.position.x -= view.bounds.width
Because the fields are now starting offscreen, add this line before you add the animation to the username layer:
flyRight.fromValue = nil
Build and run the project and the animation happens like it did before, but the text fields disappear at the end! What’s going on? Well, the animation has finished, so the layers have returned to its original value. If you used the view hierarchy debugger you’d see them hovering off to the left of the main screen.
When you animate a layer, you’re not actually seeing the layer itself animated; instead, you’re seeing a cached version of it known as the presentation layer. The presentation layer is removed from the screen once the animation completes and the original layer shows itself again.
To start, remember you’re setting the text fields to be positioned offscreen in viewWillAppear(_:):
When the animation starts, a pre-rendered animation object replaces the field and the original text field is temporarily hidden:
You can’t tap the animated field, enter any text or engage any other specific text field functionality, because it’s not the real text field, just a “phantom” visible representation.
As soon as the animation completes, it disappears from the screen and the original text field is un-hidden. The text field is right where you left it: offscreen to the left!
To solve this conundrum, you’ll need to use another CABasicAnimation property: isRemovedOnCompletion.
Setting fillMode to both instructs the animation to remain on screen after it completes and also show the animation’s first frame before its start. To complete the effect, you’ll need to set removedOnCompletion accordingly; the combination of the two will leave the animation visible on the screen.
Add the following line to viewWillAppear(), just after you set the fillMode:
flyRight.isRemovedOnCompletion = false
isRemovedOnCompletion is true by default, so the animation disappears as soon as it completes. Setting it to false and combining it with the proper fillMode keeps the animation on the screen — and visible as well.
Build and run your project now; you should see that all elements remain on the screen as expected once the animation completes:
Success! Now tap on the username field to enter your username — oh, wait. Remember the earlier note about the difference between the actual text field and the presentation layer? You can’t do anything with this pre-rendered image of a text field.
To complete the desired effect, you’ll need to remove the animation and show the real text field in its place.
Updating the Layer Model
Once you remove a layer animation from the screen, the layer falls back to its current values for position and other properties. This means that you’ll usually need to update the properties of your layer to reflect the final values of your animation.
Remove the line below from your project:
flyRight.isRemovedOnCompletion = false
Although you know how isRemovedOnCompletion works when set to false, try to avoid it whenever possible. Leaving animations on the screen affects performance, so you’ll let them be removed automatically and update the original layer’s position instead.
Next, find the line of code that adds the animation to username in viewWillAppear() and add the following line after it:
username.layer.position.x = view.bounds.size.width / 2
Next, add the code for the password field. Find the line that adds the animation to password and add the following line after it:
password.layer.position.x = view.bounds.size.width / 2
This will set the actual layers to be positioned in the middle of the screen where they belong.
Build and run your project; Uh oh. The fields have stopped animating altogether! What’s going on?
You set the animation’s fromValue to nil some time ago, but the code above updates the main layer’s position; this causes the animation to start from the center of the screen. To fix this, remove the following line:
flyRight.fromValue = nil
Build and run your project again, and this time you’ll see the fields animate as expected.
Note: If the keyboard doesn’t appear when you tap on a text field in the Simulator, you can activate it manually by navigating the menu to Hardware\Keyboard\Toggle Software Keyboard.
When possible, design your layers in Interface Builder with their final values, and use fromValue for the starting and in-between values. This reduces the complexity of keeping your model and presentation layers in sync.
Best Practices
Whoa — this was a long chapter! You tried out a ton of different layer animation techniques, and that’s just the start!
At this point you might be feeling a bit overwhelmed and asking yourself “Should I use fillMode? Should I be removing my animations? And how do I update my layer to have smooth animation completion?”
As a rule of thumb: Remove your animations and consider never using fillMode, except if the effect you want to achieve is not possible otherwise. fillMode makes your UI elements lose their interactivity and also makes the screen not reflect the actual values in your layer object.
In some rare cases when you animate non-interactive visual elements fillMode will save your bacon; you’ll read more about this in Chapter 18, “Replicating Animations.”
As for updating your layer properties: consider always doing that immediately after you add the animation to your layer. Sometimes you might get the odd flash between the initial and final animation values.
In this case, try updating your layer property to the final animation value even before adding the animation.
Key Points
-
Layer animations give you more options when it comes to creating UI animations. Unlike view animations, you can animate many additional treats such as corner radius, shadow, border width and color, border style, and more.
-
CABasicAnimationis a basic animation model class you use to describe the desired animation, which you hand off for rendering by callingCALayer.add(_, forKey:). -
Since layer animations are data models that are copied by Core Animation when added to a layer, you can reuse the same model instance to create a number of similar animations and even adjust some of its properties in between adding it to different layers.
Challenges
You covered a lot of ground in this chapter; if you want to really test that you’ve retained all of the concepts covered in each section, feel free to take on the challenges below.
Refer back to the various sections if you need some assistance, but if you’ve followed along with the exercises in this chapter, you’re more than capable of working through each of the three challenges in this chapter!
Challenge 1: Fade in the Clouds With Layer Animations
In this challenge, you’ll replace the UIKit cloud animations from Chapter 3, “Getting Started With View Animations” with layer animations instead.
If you need a recipe to follow, the steps below should give you a good starting point:
-
Remove the four UIKit animations that fade in
cloud1,cloud2,cloud3, andcloud4fromviewDidAppear(). -
Remove the four lines from
viewWillAppear()that set the clouds’alphaproperty to0.0. -
At the bottom of
viewWillAppear(), create aCABasicAnimationwithopacityas thekeypath. -
Set
fromValueto0.0,toValueto1.0, anddurationto0.5. You’ll need to setfillModeto.backwardsto hide the clouds when the app starts. -
Set the animation’s
beginTimetoCACurrentMediaTime() + 0.5and add the animation tocloud1. -
Set the animation’s
beginTimetoCACurrentMediaTime() + 0.7and add the animation tocloud2. -
Set the animation’s
beginTimetoCACurrentMediaTime() + 0.9and add the animation tocloud3. -
Set the animation’s
beginTimetoCACurrentMediaTime() + 1.1and add the animation tocloud4.
The end result should recreate the initial cloud transition with layer animations as shown below:
Challenge 2: Animating Colors
In this challenge you’ll re-create the Log In button tint animation.
First, remove your old UIKit code as shown below:
self.loginButton.backgroundColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
Then scroll down to resetForm() and remove the following line:
self.loginButton.backgroundColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
Create a new top-level function in ViewController.swift (e.g. a top-level function will be located outside the class body; add it below the delay function):
func tintBackgroundColor(layer: CALayer, toColor: UIColor)
In this new method, create a basic animation and run it on the layer parameter, keeping in mind these key requirements:
-
Animate the
backgroundColorproperty. -
Set
fromValueto the current background color:layer.backgroundColor. -
Set
toValuetotoColor.CGColor; Core Animation usesCGColorvalues for colors. -
Set the animation
durationfor1.0second. -
Add the animation to
layer. -
Don’t forget to set the
backgroundColorproperty on the layer itself so it retains the final animation color.
Now that tintBackgroundColor is complete, you can use it on your Log In button. To do that add, the following code to the very bottom of login():
let tintColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
tintBackgroundColor(layer: loginButton.layer, toColor: tintColor)
Build and run your project; you’ll see the button change its tint as soon as you tap it:
Now find the call to animate(...) within resetForm() and replace completion: nil with the following code:
completion: { _ in
let tintColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
tintBackgroundColor(layer: self.loginButton.layer, toColor: tintColor)
}
This will tint the button back to green when the animation is complete. As an added bonus, since your new function tintBackgroundColor is a top-level function, you can re-use it anywhere you like in your project!
Challenge 3: Animating Corner Radius
In this challenge you won’t recreate one of your existing view animations; instead, you’ll animate the layer specific property cornerRadius. Just like you did in Challenge 2 above, create the following new top-level function in ViewController.swift:
func roundCorners(layer: CALayer, toRadius: CGFloat)
This method will take a layer parameter and run a basic animation on it that animates the corner radius from its current value to the supplied toRadius.
Simply repeat the routine from Challenge 2, but follow these requirements instead:
-
Animate the
cornerRadiusproperty. -
Set
toValuetotoRadius. -
Set the animation
durationto0.33seconds. -
Add the animation to
layer. -
Don’t forget to set the
cornerRadiuson the layer.
Once you’ve completed roundCorners(), add the following line to the bottom of login():
roundCorners(layer: loginButton.layer, toRadius: 25.0)
This should round the button when you tap it. To reverse the effect when the authentication process is done, add the following code to resetForm() after the call to tintBackgroundColor:
roundCorners(layer: self.loginButton.layer, toRadius: 10.0)
Build and run your project; you’ll see the button appear with its initial color and corner radius:
Now tap the button to see it change in shape and color:
Once all animations complete, the button will return to its initial color and corner radius.
That’s it for this chapter; by now you have a solid understanding of how to create basic layer animations, which is the perfect starting point to tackle the animation keys and delegate methods in the next chapter!