Articles, podcasts and news about Swift development, by John Sundell.

Core Animation gems: Using replicator layers in Swift

Published on 27 Aug 2017
Basics 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 💎 - CAReplicatorLayer.

Specialization

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.

Can your UIColor do this?

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:

A row of gems

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.

Setting things up

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)

We use 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 contents:

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.

Using 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? 😉

Replicatorception

Since a 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:

Gems in a grid

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:

Animated grid of gems

Conclusion

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.

You can find all the sample code from this post on GitHub here.

Thanks for reading! 🚀