18.
Replicating Animations
Written by Marin Todorov
In this chapter you’re going to try something completely new: using a container layer that lets you replicate animations.
Let me introduce you to my favorite layer class: CAReplicatorLayer.
The idea behind CAReplicatorLayer is simple. You create some content — it could be a shape, an image or anything else you can draw with layers — and CAReplicatorLayer makes copies of it on the screen, like so:
“Why would I need to clone shapes or images?” you would ask. And you would be right to ask that; it’s not often you’d need the exact appearance of anything cloned a number of times.
CAReplicatorLayer’s superpowers come from the fact you can easily instruct it to make each clone slightly different from its ancestor.
For example, you could progressively change the tint of each copy. Your original layer could be magenta, while you progress the tint towards cyan as you create each copy.
Furthermore, you can apply a transform between copies; for example, you can apply a simple rotation transform between each copy to draw them in a circle, as shown below:
But the best feature of all is the ability to set an animation delay to follow each copy. When you set an instanceDelay of 0.2 seconds and add an animation to your original content, the first copy will animate with a delay of 0.2 seconds, the second copy will animate in 0.4 seconds, the third one in 0.6 seconds and so forth.
You can use this to create engaging and complex animations where you animate multiple elements in a synchronous manner.
In this chapter, you’re going to work on a personal assistant app that will “listen” to your questions and answer back. As a wink to Apple’s own personal assistant Siri, your project has been named Iris.
You’re going to create two different replications. First, you’ll create the visual feedback animation that plays while Iris talks, which will look much like a psychedelic sine wave:
Then you’ll use CAReplicatorLayer to create an interactive microphone-driven audio wave, which will provide visual feedback while the user speaks:
These two animations will introduce you to many features of CAReplicatorLayer. To cover every feature this layer offers would fill an entire book on its own!
But you don’t need to listen to me yammer on about how much I like creating animations with CAReplicatorLayer; it’s time to experience the magic for yourself.
Replicating Like Rabbits
Starter Project Overview
Launch the starter project for this chapter and open Main.storyboard. You’ll notice that the project setup is quite straightforward:
There is only a single view controller, which features a button and a label. The user asks their question while they hold down the button; when they release the button Iris will speak in response. The label will display the mic input levels and Iris’ answer.
Open ViewController.swift; note the button events are already connected to actions. When the user touches down on the button, actionStartMonitoring() fires; when the user lifts their finger, actionEndMonitoring() fires.
Right now ViewController doesn’t do much and simply calls startSpeaking() from within actionEndMonitoring() when the user lifts their finger.
The project features two more classes that are beyond the scope of this chapter, but they’ll help you develop a fun and functional app while letting you focus on the animation parts.
-
Assistant: The artificial intelligence assistant. It has a list of predefined amusing answers and speaks them in response to the user’s questions. -
MicMonitor: Monitors the input levels on your iPhone’s microphone and repeatedly calls a closure expression that you provide. This is where you have the chance to update the display.
Everything is ready for you to jump in and add some cool animations! First, run the project - it will ask you for microphone permissions:
Simply tap OK on the system alert:
This will get you set up for what’s coming next.
Setting up the Replicator Layer
Open ViewController.swift and add the following two properties:
let replicator = CAReplicatorLayer()
let dot = CALayer()
dot will be a simple shape drawn using the CALayer basic properties such as background and border color. replicator will help you get multiple dot copies on screen.
Next, add the following constants you’ll need to create your animation:
let dotLength: CGFloat = 6.0
let dotOffset: CGFloat = 8.0
You’ll use the first constant as the width and height of the dot layer, while the second constant holds the offset between each dot replication.
To finish off the setup, you’ll have to add the replicator layer to the view controller’s view. Add the following to viewDidLoad():
replicator.frame = view.bounds
view.layer.addSublayer(replicator)
Here, you make the replicator layer the same size as the view controller’s view and add it as a sub-layer. If you were to run the project at this point, nothing would appear to have changed; that’s because you didn’t add any visible content to replicate.
The next step is to dress up the dot layer and add it to replicator in order to display the replications. Append the following to viewDidLoad():
dot.frame = CGRect(
x: replicator.frame.size.width - dotLength,
y: replicator.position.y,
width: dotLength,
height: dotLength)
dot.backgroundColor = UIColor.lightGray.cgColor
dot.borderColor = UIColor(white: 1.0, alpha: 1.0).cgColor
dot.borderWidth = 0.5
dot.cornerRadius = 1.5
You first position the dot layer towards the right edge of the replicator and therefore, the right edge of the screen. Then you set the layer’s background color and add a border. At this point the layer will look like the following:
To see it displayed on the screen, add the following:
replicator.addSublayer(dot)
This adds dot to replicator. Run your project and you’ll see it show up:
So far so good. What you see is what you would expect from adding just about any layer to any other layer.
“But where’s the magic you promised us?” you’re asking. Hang tight — you’re almost there!
You’re going to work with three CAReplicatorLayer properties that will help you access that replication magic:
-
instanceCount: Sets the number of copies you want -
instanceTransform: Sets the transform to apply between copies -
instanceDelay: Sets the animation delay between copies
You want the replication to fill the screen, so you’ll have to divide the screen width by the offset between copies (dotOffset) to get the number of dots needed to fill up the width. This will give you more replications on an Plus-sized iPhone and fewer on an iPhone SE.
Add the following line to viewDidLoad() to set the number of copies (including the original copy):
replicator.instanceCount = Int(view.frame.size.width / dotOffset)
On an 5.5 inch screen instanceCount ends up as 51; on a 4.7 inch screen 46, and on a 4 inch screen, 40.
Run the project again; hey, where are all the copies?
No, the photocopier isn’t on the fritz again. All your copies of dot are there, but they’re all on top of each other. You’ll need to apply a transform to show them all.
Add the following to the end of viewDidLoad():
replicator.instanceTransform = CATransform3DMakeTranslation(
-dotOffset, 0.0, 0.0)
In the code above, you first set a translation transform between each replication. You then take the negative value of dotOffset for the translation on the X axis because you want the copies to progress from right to left.
replicator will consider dot and subtract 8 points from its position; the first copy will appear at this point. The second copy will appear at another 8 points to the left and so forth.
Run your project; you’ll see all copies line up in the middle of the screen, each replication translated 8 points from the previous one:
Your First Replicated Animation
To understand what instanceDelay does, you’ll add a little test animation to dot. Add the following to the end of viewDidLoad():
// This is a test animation, you're going to delete it
let move = CABasicAnimation(keyPath: "position.y")
move.fromValue = dot.position.y
move.toValue = dot.position.y - 50.0
move.duration = 1.0
move.repeatCount = 10
dot.add(move, forKey: nil)
// This is the end of the code you're going to delete
This animation simply moves dot up 50 points and repeats that action 10 times. Run the project and you’ll see all clones obediently moving across the screen like so many synchronized kill-bots.
Next you’ll add a bit of delay to the animation. Insert the following line of code:
replicator.instanceDelay = 0.02
Run your project once more and observe how all copies follow the original dot animation, but only after an ever-increasing delay:
Now that you have the basics covered, it’s time to move on to cooler animations.
Before you go on, delete the test animation you just added (use the comments above as a guide); you’re going to create much better animations in its place.
Note: Make sure you leave the line of code where you set the
instanceDelayproperty - you’ll need it for the effects you are going to build shortly.
Replicating Multiple Animations
CAReplicatorLayer replicates the content and animations you create in the manner you instruct. But it’s up to you to come up with some cool animations that would look even cooler when replicated.
In this section, you’ll work on the animation that plays while Iris speaks. To do this, you’ll combine a number of simple animations with different delays to produce the final effect.
Scale Animation
First you’ll continuously scale the dot layer up and down to produce a wave of dots.
Find startSpeaking() and add the following scale animation:
let scale = CABasicAnimation(keyPath: "transform")
scale.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
scale.toValue = NSValue(caTransform3D:
CATransform3DMakeScale(1.4, 15, 1.0))
scale.duration = 0.33
scale.repeatCount = .infinity
scale.autoreverses = true
scale.timingFunction = CAMediaTimingFunction(name: .easeOut)
dot.add(scale, forKey: "dotScale")
This is a simple layer animation like the many others in this section of the book. You scale the dot layer vertically 15-fold and run the animation continuously back and forth.
Run the project and tap the gray button; this calls actionStartMonitoring(), actionEndMonitoring() and finally your code in startSpeaking(). You should see your original dot layer animate and all copies follow with their respective delays:
Congrats — you’re off to a great start with CAReplicatorLayer!
if you want to get some extra kicks out of this animation, try changing the timing function of the animation to see what other cool waveforms you can create. For example here’s how an ease-in timing shapes the resulting effect:
Opacity Animation
Next you’ll make the original dot layer fade in and out. This will make the wave some dimension and change the alpha as it grows and shrinks to simulate light conditions. It will look much like a spinning twisty ribbon candy:
Add the following fade animation to startSpeaking():
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 1.0
fade.toValue = 0.2
fade.duration = 0.33
fade.beginTime = CACurrentMediaTime() + 0.33
fade.repeatCount = .infinity
fade.autoreverses = true
fade.timingFunction = CAMediaTimingFunction(name: .easeOut)
dot.add(fade, forKey: "dotOpacity")
You fade the dot layer from opacity 1.0 to 0.2 over the duration of the scale animation. This time around, you start the animation with a delay of 0.33 seconds; this starts the fade-out effect when the wave it at its fullest.
Run your project and enjoy the new effect as the two animations run simultaneously:
Tint Animation
If you push your imagination (and squint a little) you can imagine the wave twisting around and around on your screen. That impression would be a lot more clear if you animated its tint, as if the wave had a different color on each side.
This should be an easy enough task – all you have to do is animate the background color of dot.
Add a third animation to startSpeaking():
let tint = CABasicAnimation(keyPath: "backgroundColor")
tint.fromValue = UIColor.magenta.cgColor
tint.toValue = UIColor.cyan.cgColor
tint.duration = 0.66
tint.beginTime = CACurrentMediaTime() + 0.28
tint.fillMode = .backwards
tint.repeatCount = .infinity
tint.autoreverses = true
tint.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
dot.add(tint, forKey: "dotColor")
This animation changes the dot’s tint from magenta to cyan and back. You use a duration of 0.66 seconds; this is twice the frequency of the scaling animation and gives the impression that the color changes every time the wave “twists”.
You also give the animation a delay of 0.28 seconds; this makes the color tint animation start just before the “twist” occurs in the wave. This subtle effect provides a hint of the next color just before the wave “twists” as if there’s a bit of reflection going on.
Run the project to check out the new effect:
Animating CAReplicatorLayer Properties
So far you’ve created a pretty dazzling effect by animating the content in your replicator layer. But since CAReplicatorLayer is a layer itself, you can animate a number of its own properties too. You can animate CAReplicatorLayer’s basic properties like position, backgroundColor or cornerRadius but, you can create some interesting effects by animating some of the special properties in this layer that aren’t present in other layers.
The animatable properties unique to CAReplicatorLayer include the following:
-
instanceDelay: Animate the amount of delay between instances -
instanceTransform: Change the transform between replications on the fly -
instanceColor: Change the blend color used for all instances -
instanceRedOffset,instanceGreenOffset,instanceBlueOffset: Apply a delta to apply to each instance color component -
instanceAlphaOffset: Change the opacity delta applied to each instance
In this section, you’ll animate the instance transform to make the speech wave even more psychedelic! Add one more animation to the end of startSpeaking():
let initialRotation = CABasicAnimation(keyPath:
"instanceTransform.rotation")
initialRotation.fromValue = 0.0
initialRotation.toValue = 0.01
initialRotation.duration = 0.33
initialRotation.isRemovedOnCompletion = false
initialRotation.fillMode = .forwards
initialRotation.timingFunction = CAMediaTimingFunction(name: .easeOut)
replicator.add(initialRotation, forKey: "initialRotation")
This animation affects just the rotation component of the instance transform; that is, it preserves the translation component you set for the instances in viewDidLoad() and only animates the rotation.
You animate the rotation between instances from 0.0 radians to 0.01 radians. Each replication will appear slightly rotated compared to its neighbor.
Run your project; enjoy your replicating animations on a totally new level — or should I say curve?
The instanceRotation animation above looks nice — but when I say psychedelic, I mean PSYCHEDELIC! What if you were to combine the effect of all running replication animations AND twist and spin the wave at the same time?
Add the animation below to complete the effect:
let rotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
rotation.fromValue = 0.01
rotation.toValue = -0.01
rotation.duration = 0.99
rotation.beginTime = CACurrentMediaTime() + 0.33
rotation.repeatCount = .infinity
rotation.autoreverses = true
rotation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
replicator.add(rotation, forKey: "replicatorRotation")
Here, you run a second animation on instanceTransform.rotation that starts once the first animation has finished. You then animate the transform rotation from 0.01 radians (the final value of the first animation) to -0.01 radians and back. This gives the speech animation a final, crazy boost:
This is an outstanding (if not hypnosis-inducing) animation, and you created it by combining just a few select animations. The trick is to know which properties to animate and how to select the correct delays and durations to get the effect you’re looking for.
Note: Personally, I think the effect looks best if I rotate my head
45degrees right. But be careful not to watch it too long - it could make you dizzy!
All that’s left is to give your not-so-helpful assistant, Iris, the ability to listen and to respond. First you’ll give the power of speech to Iris. The starter project class named Assistant will help you do that. Add the following to the top of startSpeaking():
let answer = assistant.randomAnswer()
meterLabel.text = answer
assistant.speak(answer, completion: endSpeaking)
speakButton.isHidden = true
You first get a random answer from the Assistant class and visualize it via the meterLabel. Then you call speak(_:completion:) on assistant; passing in endSpeaking() as the completion parameter to be called when speaking is finished.
Next you’ll add the code in endSpeaking() that removes all running animations and gracefully returns the wave to its initial state.
Insert the following into endSpeaking():
replicator.removeAllAnimations()
This removes the animations on instanceTransform of replicator.
Next you need to animate the dot layer back to its original scale. Add:
let scale = CABasicAnimation(keyPath: "transform")
scale.toValue = NSValue(caTransform3D: CATransform3DIdentity)
scale.duration = 0.33
scale.isRemovedOnCompletion = false
scale.fillMode = .forwards
dot.add(scale, forKey: nil)
For this animation you don’t specify a fromValue; this means that Core Animation will start the animation from the current value and animate the transform to CATransform3DIdentiy. This is necessary because at any given time each of the replicated instances has a different transform. Omitting fromValue animates each replication from its current scale to the animation’s final value.
Finally, add the following code to remove the rest of the currently running animations from dot and reset the speak button state:
dot.removeAnimation(forKey: "dotColor")
dot.removeAnimation(forKey: "dotOpacity")
dot.backgroundColor = UIColor.lightGray.cgColor
speakButton.isHidden = false
Run the project again; this time, Iris will speak a random answer right back at you!
When Iris is done speaking, the wave will gracefully animate back to its initial state:
Interactive Replication Animations
Right now you need to press the speak button each time to see and hear Iris’ answer. But you don’t get to actually ask her anything, which, to be honest, is the really fun part.
In this final section you’ll create an animation that displays the microphone input while you ask Iris your questions.
Note: In case the microphone in the iOS simulator does not work for you use a physical device to test your app in this part of the chapter.
Add the following to actionStartMonitoring():
dot.backgroundColor = UIColor.green.cgColor
monitor.startMonitoringWithHandler { level in
self.meterLabel.text = String(format: "%.2f db", level)
}
The above method fires when the user presses on the speak button. To indicate the app is “listening”, you change the dot layer color to green. Then you call startMonitoringWithHandler() on the monitor instance.
Note: The
MicMonitorclass is pretty simple – peek inside MicMonitor.swift if you’re interested to see how it gets the microphone levels.
The closure block you provide as a parameter executes repeatedly and gets the current microphone level as a parameter.
Run the app and hold the button; speak to the device and you’ll see the current mic levels displayed.
So far so good; all you need now is to use the level variable to animate the replicator content accordingly.
First of all, you’ll need to normalize the mic level to something you can use for your animations. level has a value in the rage of -160.0 db to 0.0 db, -160.0 db being the quietest and 0.0 db meaning extremely loud sound.
Add an extra line of code to the handler block to convert the level value to something useful and store it in scaleFactor so that the complete block looks like this:
monitor.startMonitoringWithHandler { level in
self.meterLabel.text = String(format: "%.2f db", level)
let scaleFactor = max(0.2, CGFloat(level) + 50) / 2
}
scaleFactor will store values between 0.1 and 25.0. You can use this to scale dot to a reasonable size to represent the microphone input levels.
Add the following instance property to the ViewController class:
var lastTransformScale: CGFloat = 0.0
For the scaling animation, you’ll need to save the last scaled value to this property. Since you’ll constantly overwrite the running scale animation, you need to keep track of the last value the layer was supposed to scale to.
Now jump back to the microphone handler closure and add to it the following code, which animates from the last transform to the new one you calculated from the current microphone levels:
let scale = CABasicAnimation(keyPath: "transform.scale.y")
scale.fromValue = self.lastTransformScale
scale.toValue = scaleFactor
scale.duration = 0.1
scale.isRemovedOnCompletion = false
scale.fillMode = .forwards
self.dot.add(scale, forKey: nil)
This layer animation runs only on the y-axis of the scale component of the dot layer transform. You animate the scale from the last value used to the current scaleFactor.
Finally, still inside the handler, add the following code to save the current scaleFactor value for the next handler call:
self.lastTransformScale = scaleFactor
This code ought to bring the replicator layer to life. Run the project, hold the button and speak.
You’ll see an actual audio wave show up on screen, all handled by the replicator layer while you simply animate the original dot layer:
But when you let go of the speak button, the result is somewhat baffling:
Aha — you need to reset the animations and stop monitoring the microphone. You can take care of that in actionEndMonitoring().
Insert the following at the top of that method:
monitor.stopMonitoring()
dot.removeAllAnimations()
Here you disable the microphone monitor by calling stopMonitoring() and removing all animations running on the dotlayer. This way the app can move on and display the Iris animation.
Give the app a try. Isn’t talking to Iris a lot of fun?
The microphone input animation ends somewhat abruptly, but you’ll get to fix that in the challenges below.
Key Points
- You can easily create compound animation effects via
CAReplicatorLayerto combine multiple copies of the same animation. - You set number and variations between the animation replications via the
instanceCount,instanceTransformandinstanceDelayproperties onCAReplicatorLayer. - Besides animating properties on the original animation, you can animate also properties on the replicator layer itself.
Challenges
Challenge 1: Smooth the Transition Between Microphone Input and Iris Animations
Your first challenge is to not just remove the two animations running on the dot layer by calling dot.removeAllAnimations(), but to animate the wave back to a state suitable for the next animation to be run.
Take this challenge in three steps:
-
First, delete the line where you remove the running animations on dot from
actionEndMonitoring(). -
Then, in its place, animate the scale of dot back to a value of
1.0on the y-axis. Leave the animation on the screen to wait for the Iris animation to start — don’t remove it when it’s done. -
Finally, add another animation that changes the dot tint from green to magenta. For this animation, use a
fillModeof.backwards— without it, the replicator layer will reset the tint to the final value retroactively.
Mind the durations of these two new animations. They should finish before the Iris animation starts.
When you’ve completed this challenge, the two animations should blend nicely into each other like so:
Section Conclusion
This wraps up the basic layer animations section. You’ve been through a lot — and learned a ton of things along the way!
In this section of the book you covered:
-
Basic movement, fading, rotation, and scaling animations
-
Groups and keyframe animations
-
Shapes, masks, and gradient animations
-
Stroke and path animations
-
Replicating animations