Core Animation gems: Using replicator layers in SwiftBasics article available: Animations
If you have ever pushed a pixel onto the screen of an Apple device, you have used Core Animation - either directly or indirectly. Like most of Apple's Core family of frameworks, it's one of those essential technologies that underpin so many of the tools that we use on a daily basis - no matter which platform we're targeting.
While there's a ton of great tutorials and resources out there about the most commonly used APIs of Core Animation - such as using
CALayer to perform more low-level rendering for a
UIView, or using the animation APIs like
CAKeyframeAnimation - there are also several really useful features that are not as widely known.
In this new (non-consecutive) series of posts - "Core Animation Gems" - we'll take a closer look at some of those features and APIs, and how they can be used to solve problems related to animation and rendering in a nice way. This week, let's kick it off with the first 💎 -
The architecture of Core Animation's layer classes is very much centered around the idea of specialization. You have the root class -
CALayer - which is basically a generic canvas that anything can be drawn onto, then you have a range of subclasses that all specialize in a certain kind of rendering.
CAReplicatorLayer specializes in drawing multiple copies of an original layer (hence it being a "replicator"), in an efficient - hardware accelerated - manner. It's super useful when drawing things like tiled backgrounds, patterns or other things that should be repeated multiple times. I even use it to implement the texture tiling feature of my upcoming open source Swift game engine.
A very common way of implementing backgrounds with repeated images is by using
UIColor, which has an initializer that takes a
patternImage. It enables you to treat a repeated sequence of images as a color, and use it as a background, like this:
view.backgroundColor = UIColor(patternImage: image)
While that way of drawing repeated images can be exactly what you're looking for in some situations, in others you might want to do something a bit more complex - and this is where
CAReplicatorLayer comes in.
Let's say we want to draw a pattern consisting of a repeated image, but we also want to tint it with a shade of blue, causing the pattern to form a gradient from the original color of the image until it's completely tinted, like this:
This can easily be accomplished without any custom Core Graphics drawing code, using a replicator layer. Let's dive in and take a look at what the implementation looks like.
We'll start by setting up our replicator layer as a sublayer of a
UIView's layer, in order to render it on screen (the sample code in this post will target iOS/tvOS, but it works pretty much the exact same way for macOS):
let replicatorLayer = CAReplicatorLayer() replicatorLayer.frame.size = view.frame.size replicatorLayer.masksToBounds = true view.layer.addSublayer(replicatorLayer)
masksToBounds above to avoid over-drawing in case the size of the pattern image doesn't exactly line up with the size of the view.
We also have to give the replicator layer a sublayer to replicate. For that, we setup a simple layer that renders an image as its
let imageLayer = CALayer() imageLayer.contents = image.cgImage imageLayer.frame.size = image.size replicatorLayer.addSublayer(imageLayer)
If we now run the above code (for example in a Playground) we'll see the image being rendered, but only a single copy of it. This is because we also need to tell our replicator layer how many copies (or instances) we'd like it to render. Let's set it up so that we render enough copies to fill the width of our view:
let instanceCount = view.frame.width / image.size.width replicatorLayer.instanceCount = Int(ceil(instanceCount))
We will now have multiple copies rendered, but we're still not able to see more than one - since they are, per default, all rendered stacked on top of each other. To fix that, we need the final piece of the puzzle - and one of the most powerful features of
CAReplicatorLayer - instance offsets & transforms.
Each instance that a replicator layer is rendering can be transformed in a few different ways. This enables you to create quite complex patterns and animate them in interesting ways.
To achieve the gradient pattern we're looking for, we'll apply a
CATransform3D transform to each instance - shifting them to the right - and reduce the red & green color component of each instance's tint color, like this:
// Shift each instance by the width of the image replicatorLayer.instanceTransform = CATransform3DMakeTranslation( image.size.width, 0, 0 ) // Reduce the red & green color component of each instance, // effectively making each copy more and more blue let colorOffset = -1 / Float(replicatorLayer.instanceCount) replicatorLayer.instanceRedOffset = colorOffset replicatorLayer.instanceGreenOffset = colorOffset
And with that, we have a horizontally repeating gradient pattern! 🎉 But let's take it a step further, shall we? 😉
CAReplicatorLayer is a subclass of
CALayer, you can nest them within each other. This enables you to create even more complex patterns in multiple dimensions. For example, let's say we wanted to extend our original pattern to also repeat vertically using another tint color gradient, like this:
All we'd have to do is to simply wrap our original replicator layer into another, which offsets each instance vertically and reduces each instance's blue color component:
let verticalReplicatorLayer = CAReplicatorLayer() verticalReplicatorLayer.frame.size = view.frame.size verticalReplicatorLayer.masksToBounds = true verticalReplicatorLayer.instanceBlueOffset = colorOffset view.layer.addSublayer(verticalReplicatorLayer) let verticalInstanceCount = view.frame.height / image.size.height verticalReplicatorLayer.instanceCount = Int(ceil(verticalInstanceCount)) verticalReplicatorLayer.instanceTransform = CATransform3DMakeTranslation( 0, image.size.height, 0 ) verticalReplicatorLayer.addSublayer(replicatorLayer)
Now that we have a grid of gradually tinted gems - let's have some fun with it 😀 We are using Core Animation after all, so adding some animations seems like the right thing to do.
Using only a few lines of code, we can achieve some really interesting effects. First, let's set our replicators (both the horizontal and vertical) to add a slight delay to all animations applied to the layer they're replicating:
let delay = TimeInterval(0.1) replicatorLayer.instanceDelay = delay verticalReplicatorLayer.instanceDelay = delay
Then, let's add a
CABasicAnimation that makes the image layer scale up and down repeatedly:
let animation = CABasicAnimation(keyPath: "transform.scale") animation.duration = 2 animation.fromValue = 1 animation.toValue = 0.1 animation.autoreverses = true animation.repeatCount = .infinity imageLayer.add(animation, forKey: "hypnoscale")
Since the animation applied to the image layer also gets replicated by the replicator layers, and since we applied a slight delay to all instances, we get a pretty crazy result:
CAReplicatorLayer can be a really great - and easy to use - tool for certain types of rendering and animations. You could - for example - use it to implement tiled backgrounds, custom loading animations or other things that usually require some form of repeated pattern.
Like most of the gems that we'll take a look at in this series of posts, replicator layers are hardly something that you'll use every time you need to render an image or a pattern - but it's definitely a good option to keep in mind if you need to do something a bit more complex with great performance.
What do you think? Do you have some special Core Animation 💎 that you'd like me to write more about in an upcoming post? Let me know, along with any other questions, feedback or comments that you might have - on Twitter @johnsundell.
Thanks for reading! 🚀