After my extensive write up about
CATransaction, I received a request to write a similar post about
CAShapeLayer. Shape layers are very useful for creating UI elements, and because they are vector-based, they are resolution independent. Not only that, but many shape layer properties are fully animatable, making them perfect for things like icon transitions.
CAShapeLayer has a lot to offer, I’ll be breaking up our exploration of everything this class has to offer into three parts: Part I, this post, will focus on everything involved with creating shape layers. Part II will focus on animating shape layers, which is perhaps their most powerful capability. And finally, Part III will be a showcase of sorts that demonstrates several non-trivial examples of creating and animating shape layers as well as different applications for shape layers.
Let’s get started.
- What Shape Layers Are
- Creating Shape Layers
What Shape Layers Are
Shape layers are layers capable of defining shapes as vectors. Because they’re defined as vectors, they are resolution-independent layers. At render time, Core Animation will create a bitmap that is the appropriate size to match the device’s screen scale. The result is that shape layers will always look crisp when drawn.1
Shape layers can be stroked and filled, and their lines’ properties can be adjusted as well. In true Core Animation fashion, shape layers also have many animatable properties, which allows developers to easily create compelling animations.2 Most importantly,
CAShapeLayer is, unsurprisingly, rendered entirely on the GPU, making it very fast.3 With that said, let’s see how to create shape layers.
Creating Shape Layers
Shape layers themselves are fairly easy to create. They take no initialization parameters:
CAShapeLayer is a vector-drawn layer, we don’t need to worry about setting its
contentsScale property. Regardless of the value, shape layers will always be drawn at the device’s main screen scale.
In its current form, our shape layer doesn’t do anything, so let’s set some of its properties.
A shape layer’s path is the heart of what makes it a shape. This property takes a
CGPath, so it uses the same path logic as Core Graphics. Alternately, you can use
UIBezierPath’s simpler API and ask it to return a
CGPath representation when you’re finished.4
But what is a path? You can think of a path as a bunch of line segments or cubic Bézier curves, which may or may not be connected together. Apple’s documentation has a good guide on drawing shapes with Bézier paths. In short, however, we can draw simple shapes—like rectangles and circles—and much more complex shapes—like a multi-point star.
Here’s how we could create shape layers for each of those example paths:
Certainly, the first two examples could be trivially accomplished using simple
CALayer properties like
cornerRadius, but I just included them to demonstrate that paths can be used to create any type of shape.
The word path may seem like a bit of a misnomer, actually. Intuitively, a path makes sense when it is a single, contiguous line: you start at one point and keep drawing a line until you reach some other point, moving straight or curving around however you want along the way. But this doesn’t have to be the case.
UIBezierPath can have any number of “path segments” (or subpaths) so you can effectively draw as many shapes or lines as you want in a single path object:
How should you determine how many paths to include in a single shape layer? Well, it depends. If your content is static and not likely to change, and if it’s okay for every path to share the same style parameters, then a single shape layer may be sufficient.5 Generally, however, I prefer to draw single, discrete shapes in their own shape layer so they’re easier to style, manage, and animate.
Paths do not need to connect their end points back to their starting points. A path that connects back to its starting point is called a closed path, and one that does not is called an open path. Calling
closePath() will force the path to be closed by simply drawing a straight line from the current point to the starting point. If, instead, you call
moveToPoint(_:), the path will remain open, and you can begin drawing a new path segment from that new point.
The fill color is just that: the color that fills the path.
Perhaps a little-known feature of
CGColor) is its ability to create a “color” from a pattern image.7 This is a nice way to give a shape a little bit of texture:
With a seamless pattern image in hand, it’s trivial to fill a path with that instead of a solid color:
Recall that there are open and closed paths. Attempting to fill an open path does not cause the fill to overflow outside of the path’s boundary similar to how Photoshop or MS Paint does.
CAShapeLayer will close the current subpath by simply drawing a straight line from the current point to the starting point before filling the shape:
The fill rule determines how the path fills in its regions with color. Fill rules are slightly technical, but basically, in complex paths, if a region of a path is enclosed by another region, these rules determine which regions are filled with the fill color. This site has a decent visual explanation of both these rules, so feel free to check that out.
This one’s obvious: this determines the width of the line used to stroke the path.
Note that shape layers stroke their paths evenly on either side. That is, a stroke is centered about its path, so half the line width will appear on either side.8 To create a hairline stroke on a Retina display, for example, you would pick a line width of 0.5 (for @2x displays, of course), which is what the star shape above is using.
A shape layer’s line join is pretty simple to understand: it determines the shape that joined segments of a path have.
CAShapeLayer supports three different styles for line joins: miter, round, and bevel.
Miter line joins have a special complementary property called the miter limit. Essentially, the miter limit is a threshold value
CAShapeLayer uses to decide when to convert a miter join to a bevel join. If the length of a particular miter, divided by the line width, is greater than the miter limit, then that line join will be drawn with a bevel instead.
The documentation for
miterLimit doesn’t go into much detail about the calculations involving this property. However, I observed a direct correlation between
CAShapeLayer’s behavior and the SVG spec’s behavior when it comes to miter limit. The documentation for SVG’s
stroke-miterlimit attribute goes into a bit more detail about the calculation involved and how exactly miter limit and line width are related.
Line cap determines how the end points of open paths are stroked: butt, round, or square.
(The thin, white lines are added for emphasis only and don’t have anything to do with how line caps render.)
Line Dash Pattern
A line dash pattern allows you to make a shape layer draw its lines with an arbitrary dash pattern instead of a solid line. This property is an array of alternating lengths—in user space9—that determine how long to stroke the line and how long to not stroke it. The pattern is repeatedly until it reaches the end of the path.
circleShapeLayer4’s line dash patterns. They contain only one value, an odd number.
CAShapeLayer doesn’t require actual pairs of stroked-unstroked values for the pattern; it’ll just cycle back to the beginning of the array and keep going.
Line Dash Phase
Line dash phase is a single number that specifies an offset—again, in user space—applied to the line dash pattern. This allows you to shift where in the pattern the shape layer will start when drawing lines. Note that this value doesn’t alter the total length of the pattern—that is, it does not shorten the pattern. When the end of the pattern is reached, it cycles back to the beginning of the pattern, not back to the offset specified by the line dash phase.
Stroke color should be very obvious: the color that’s used to stroke the path.
Just as with fill color, a pattern image can be used in lieu of a solid color to create interesting styles and effects for strokes.
Stroke Start and End
Stroke start and end are interesting properties. They effectively define how much of the specified path should be stroked. Each value is represented in unit space; i.e., both stroke start and stroke end must be a value between 0.0 and 1.0. The default values for stroke start and end are 0.0 and 1.0, respectively, meaning the entire path will be stroked.
It doesn’t matter how complex the path is;
CAShapeLayer will simply stroke the path at the percentages covered in the stroke start and end range.
By now, it should be clear that
CAShapeLayer has a lot to offer. If you are already familiar with Core Graphics, shape layers should be relatively familiar. Of course, Core Graphics has a lot more functionality that
CAShapeLayer does not offer. However, the fact that
CAShapeLayer is part of the Core Animation ecosystem and efficiently composites itself on the GPU instead of the CPU10 makes it a valuable class nonetheless. And we have yet to explore its animation capabilities, something Core Graphics cannot do.
So what’s next? As I mentioned at the beginning of this post, Part II will be a full analysis of
CAShapeLayer’s animatable properties, which is perhaps its most exciting aspect. After that, I’ll be taking some time to put together several examples of complex or interesting uses of shape layers, including some applications which may not be immediately obvious.
Unless they are forced to do otherwise. To force
CAShapeLayer—or any other layer class, for that matter—to draw at a different scale, set
rasterizationScaleaccordingly. Rasterization can affect performance, especially during animations, so use them judiciously. ↩
Interestingly, because iOS 7 ushered in a new era of flatter design aesthetics, designers are now creating many UI elements that are simpler. This means such elements are much easier to animate now. Previously, rich textures and lighting meant animating these types of designs was significantly more complex. ↩
In fact, the documentation makes a point to mention that
CAShapeLayerfavors speed over accuracy. So if your designs require absolute accuracy, you’re better off drawing with Core Graphics instead. ↩
UIBezierPathis just a wrapper around
CGPath, though they are not toll-free bridged. ↩
Astute readers may point out that
UIBezierPathitself defines its own style properties, such as line width and line cap. Conceivably, a single shape layer should be able to reference these properties to style multiple subpaths differently. However,
CAShapeLayerdisregards these properties in favor of applying its own properties universally to the entire path. ↩
Keep in mind that
fillColor, like most Core Animation properties, needs to be a Core Foundation type, i.e.,
For this reason, I always thought
UIColorshould have been named
UIFill, since a pattern image isn’t really a color. Ah well… ↩
This is contrary to how
CALayerborders work: borders always draw starting at the bounds edges moving inward. ↩
That is to say, they are measured in the coordinate space of the shape layer itself, which measures things in points and converts to screen space as necessary at render time. This is similar to how UIKit works: agnostic to screen space units. ↩
When it comes to drawing, the GPU is not always faster than the CPU. In fact, an initial attempt to port Core Graphics over to the GPU on OS X, called Quartz 2D Extreme, was abandoned after the team realized that the GPU was not best-suited for certain kinds of drawing. Still,
CAShapeLayer’s true rendering strength lies in the fact that it can remain GPU-resident for compositing. Drawing still occurs on the CPU, but it does so within the render server, which has its own dedicated thread. Having to draw on the CPU in your app’s process for every frame and uploading that bitmap to the GPU is the reason that Core Graphics is poorly-suited for things like animation. And it is this reason that
CAShapeLayer, if at all possible as a substitute for Core Graphics, should be used when high performance is desired. ↩