Core Animation provides a great set of lower-level APIs that let you draw (though you don’t actually call the pixel-on-screen renderer), animate, mask, all with great efficiency! CGMutablePath might seem a bit intimidating at first, but with a little bit of practice, you’ll find it’s actually quite simple, so long as you can visualise the drawing code.
Today we’re gonna have a look at a little less known class from Core Animation – CAReplicatorLayer. CAReplicatorLayer allows you to add a CALayer or CAShapeLayer as a sublayer, and then the replicator will replicate that view! You define how many times the layer should be replicated, and each view should change upon replication. Because you’re just copying a single layer over and over, it provides you with great performance, and is easy to change in the future!
Let’s take a quick look. Suppose our designer has this activity spinner for our new app.
How would you make this? A bunch of UIViews with a corner radius positioned at an angle and radius to the centre of the entire view? And then animate each one? Or maybe draw the whole thing, each individual circle, using CAShapeLayers or CALayers? Or how about going crazy and using a CAEmitterLayer? I actually had to build something very similar to this, and asked around the other developers about how they would do it – those are their answers.
But this is a perfect use case for CAReplicatorLayer. Let’s take a look at how we would do this.
First make a UIView subclass and add these five properties:
Now override the draw method. In here we’re just gonna setup our replicatorLayer circle layer, and circleMask object.
Let’s break this down a bit. First we’re just setting the frame of our replicator layer. Then we set the instance count of it. This is just how many times the object we give it will be replicated. Notice how we also specify the instanceTransform of our replicator layer. CAReplicatorLayers allow each replication to be transformed in two ways – its colour (including alpha), and its transform. The colour change is specified with an offset. So you’re able to specify how much each colour channel should offset as the index of replications increases. Here’s a full list of how each instance can be modified:
So we calculate the transform of each instance by using the CATransform3DMakeRotation, and specify the angle, which we calculate in the previous line. That’s all the setup of CAReplicatorLayer needed! Now all we need to do is define our CALayer circle with a position and size and colour. Note I’m setting the cornerRadius property here rather than masking. You can definitely mask if you want, but I’m using the cornerRadius here just because it’s an expensive operation, but with CAReplicatorLayer we’re only doing it once for all the circles.
Finally we just add our circle CALayer as a sublayer of our replicator layer, and add the replicator layer as a sublayer of our view’s layer.
Build and run and you’ll seeeeeeeee this
Great. Look how little code we wrote to build that. And even better is this is a very efficient view, since we’ve just replicated one drawing over and over. So that’s a good start, but obviously this thing isn’t doing anything. How do we animate it? This is where CABasicAnimation comes into play. We’ve had a look at this before – it’s a string based API which seems kinda weird after coming from Swift. But it’s still pretty easy to use. We’re gonna make use of extensions to make this easier for us.
Create an extension on CALayer and add this function:
You could put this in a specialised file, or even in the same file as this view if you want. Now to use it. Back in our draw function, just before we add the replicator layer as a sublayer of our view’s layer, add these two lines:
Now build and run and voila! Your final animation. Hopefully you can imagine how you could extend this out a bit. These dots could be in a line, or you could even use this to animate and make the three little dots to indicate the other user is typing, for example.
You can grab the full source code below
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class RepeatingCircle: UIView { | |
let replicatorLayer = CAReplicatorLayer() | |
let circle = CALayer() | |
let duration: TimeInterval = 1.0 | |
let circleSize: CGFloat = 15 | |
let instanceCount = 20 | |
override func draw(_ rect: CGRect) { | |
replicatorLayer.frame = rect | |
replicatorLayer.instanceCount = instanceCount | |
replicatorLayer.instanceDelay = duration / CFTimeInterval(instanceCount) | |
let angle = -CGFloat.pi * 2 / CGFloat(instanceCount) | |
replicatorLayer.instanceTransform = CATransform3DMakeRotation(angle, 0, 0, 1) | |
circle.frame = CGRect(origin: CGPoint.zero, | |
size: CGSize(width: circleSize, height: circleSize)) | |
circle.backgroundColor = UIColor.blue.cgColor | |
circle.cornerRadius = circleSize / 2 | |
replicatorLayer.addSublayer(circle) | |
circle.add(animation: "transform.scale", fromValue: 1, toValue: 0.2, duration: duration) | |
circle.add(animation: "opacity", fromValue: 1, toValue: 0, duration: duration) | |
self.layer.addSublayer(replicatorLayer) | |
} | |
} | |
extension CALayer { | |
func add(animation: String, fromValue: Double, toValue: Double, duration: Double) { | |
let animation = CABasicAnimation(keyPath: animation) | |
animation.fromValue = fromValue | |
animation.toValue = toValue | |
animation.duration = duration | |
animation.repeatCount = Float.greatestFiniteMagnitude | |
self.add(animation, forKey: nil) | |
} | |
} |