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.

Because 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

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:

let shapeLayer = CAShapeLayer()
Creating a shape layer

Because 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.

Path

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.

Example paths

Here’s how we could create shape layers for each of those example paths:

let shapeLayer = CAShapeLayer()

shapeLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 120.0, height: 120.0)
shapeLayer.lineWidth = 2.0
shapeLayer.fillColor = nil
shapeLayer.path = UIBezierPath(rect: shapeLayer.bounds).CGPath
Example square shape layer
let shapeLayer = CAShapeLayer()

shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: 120.0, height: 120.0)
shapeLayer.lineWidth = 2.0
shapeLayer.fillColor = nil

let arcCenter = shapeLayer.position
let radius = shapeLayer.bounds.size.width / 2.0
let startAngle = CGFloat(0.0)
let endAngle = CGFloat(2.0 * M_PI)
let clockwise = true

let circlePath = UIBezierPath(arcCenter: arcCenter, 
                                 radius: radius,
                             startAngle: startAngle,
                               endAngle: endAngle,
                              clockwise: clockwise)

shapeLayer.path = circlePath.CGPath
Example circle shape layer
let shapeLayer = CAShapeLayer()

shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: 120.0, height: 120.0)
shapeLayer.lineWidth = 2.0
shapeLayer.fillColor = nil

let starPath = UIBezierPath()

let shapeBounds = shapeLayer.bounds
let center = shapeLayer.position

let numberOfPoints = CGFloat(5.0)
let numberOfLineSegments = Int(numberOfPoints * 2.0)
let theta = CGFloat(M_PI) / numberOfPoints

let circumscribedRadius = center.x
let outerRadius = circumscribedRadius * 1.039
let excessRadius = outerRadius - circumscribedRadius
let innerRadius = CGFloat(outerRadius * 0.382)

let leftEdgePointX = (center.x + cos(4.0 * theta) * outerRadius) + excessRadius
let horizontalOffset = leftEdgePointX / 2.0

// Apply a slight horizontal offset so the star appears to be more
// centered visually
let offsetCenter = CGPoint(x: center.x - horizontalOffset, y: center.y)

// Alternate between the outer and inner radii while moving evenly along the
// circumference of the circle, connecting each point with a line segment
for i in 0..<numberOfLineSegments {
    let radius = i % 2 == 0 ? outerRadius : innerRadius

    let pointX = offsetCenter.x + cos(CGFloat(i) * theta) * radius
    let pointY = offsetCenter.y + sin(CGFloat(i) * theta) * radius
    let point = CGPoint(x: pointX, y: pointY)

    if i == 0 {
        starPath.moveToPoint(point)
    } else {
        starPath.addLineToPoint(point)
    }
}

starPath.closePath()

// Rotate the path so the star points up as expected
var pathTransform  = CGAffineTransformIdentity
pathTransform = CGAffineTransformTranslate(pathTransform, center.x, center.y)
pathTransform = CGAffineTransformRotate(pathTransform, CGFloat(-M_PI_2))
pathTransform = CGAffineTransformTranslate(pathTransform, -center.x, -center.y)

starPath.applyTransform(pathTransform)

shapeLayer.path = starPath.CGPath
Example star shape layer

Certainly, the first two examples could be trivially accomplished using simple CALayer properties like borderWidth, borderColor, and cornerRadius, but I just included them to demonstrate that paths can be used to create any type of shape.

Subpaths

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:

A single path containing multiple subpaths
let shapeLayer = CAShapeLayer()

shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: 300.0, height: 300.0)
shapeLayer.lineWidth = 2.0
shapeLayer.fillColor = nil

let circlePath = ...        // Refer to code snippet above
let squarePath = ...        // Refer to code snippet above
let starPath = ...          // Refer to code snippet above

let shapeLayerPath = UIBezierPath()
shapeLayerPath.appendPath(circlePath)
shapeLayerPath.appendPath(squarePath)
shapeLayerPath.appendPath(starPath)

shapeLayer.path = shapeLayerPath.CGPath
A single shape layer containing multiple subpaths

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.

Open Paths

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.

Example of open paths
let shapeLayer = CAShapeLayer()

shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: 120.0, height: 120.0)
shapeLayer.lineWidth = 2.0
shapeLayer.fillColor = nil

let openSquarePath = UIBezierPath()
openSquarePath.moveToPoint(CGPointZero)
openSquarePath.addLineToPoint(CGPoint(x: 0.0, y: 120.0))
openSquarePath.addLineToPoint(CGPoint(x: 120.0, y: 120.0))
openSquarePath.addLineToPoint(CGPoint(x: 120.0, y: 0.0))

shapeLayer.path = openSquarePath.CGPath
Example open path square shape layer
let shapeLayer = CAShapeLayer()

shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: 120.0, height: 120.0)
shapeLayer.lineWidth = 2.0
shapeLayer.fillColor = nil

let arcCenter = CGPoint(x: 60.0, y: 60.0)
let radius = CGFloat(60.0)
let startAngle = CGFloat(0.0)
let endAngle = CGFloat(-M_PI)
let clockwise = false

let openCirclePath = UIBezierPath(arcCenter: arcCenter,
                                     radius: radius,
                                 startAngle: startAngle,
                                   endAngle: endAngle,
                                  clockwise: clockwise)

shapeLayer.path = openCirclePath.CGPath
Example open path circle shape layer
let shapeLayer = CAShapeLayer()

shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: 120.0, height: 120.0)
shapeLayer.lineWidth = 2.0
shapeLayer.fillColor = nil

let starPath = UIBezierPath()

let shapeBounds = shapeLayer.bounds
let center = shapeLayer.position

let numberOfPoints = CGFloat(5.0)
let numberOfLineSegments = Int(numberOfPoints * 2.0)
let theta = CGFloat(M_PI) / numberOfPoints

let circumscribedRadius = center.x
let outerRadius = circumscribedRadius * 1.039
let excessRadius = outerRadius - circumscribedRadius
let innerRadius = CGFloat(outerRadius * 0.382)

let leftEdgePointX = (center.x + cos(4.0 * theta) * outerRadius) + excessRadius
let horizontalOffset = leftEdgePointX / 2.0

// Apply a slight horizontal offset so the star appears to be more
// centered visually
let offsetCenter = CGPoint(x: center.x - horizontalOffset, y: center.y)

// Alternate between the outer and inner radii while moving evenly along the
// circumference of the circle, connecting each point with a line segment,
// skipping the last two segments
for i in 0..<(numberOfLineSegments - 2) {
    let radius = i % 2 == 0 ? outerRadius : innerRadius

    let pointX = offsetCenter.x + cos(CGFloat(i) * theta) * radius
    let pointY = offsetCenter.y + sin(CGFloat(i) * theta) * radius
    let point = CGPoint(x: pointX, y: pointY)

    if i == 0 {
        starPath.moveToPoint(point)
    } else {
        starPath.addLineToPoint(point)
    }
}

// Rotate the path so the star points up as expected
var pathTransform  = CGAffineTransformIdentity
pathTransform = CGAffineTransformTranslate(pathTransform, center.x, center.y)
pathTransform = CGAffineTransformRotate(pathTransform, CGFloat(-M_PI_2))
pathTransform = CGAffineTransformTranslate(pathTransform, -center.x, -center.y)

starPath.applyTransform(pathTransform)

shapeLayer.path = starPath.CGPath
Example open path star shape layer

Fill

A shape layer can fill its path with a color. There are two properties that affect fill, namely fillColor6 and fillRule.

Fill Color

The fill color is just that: the color that fills the path.

Example paths filled with solid colors

Perhaps a little-known feature of UIColor (and 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:

Example path filled with a pattern

With a seamless pattern image in hand, it’s trivial to fill a path with that instead of a solid color:

let patternImage = UIImage(named: "red-diagonal-stripe-pattern-image")

let circleShapeLayer = ...      // Refer to code snippet above
circleShapeLayer.fillColor = UIColor(patternImage: patternImage).CGColor
Example circle shape layer filled with a pattern image

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:

Example of open paths that are filled

Fill Rule

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.

Example paths using non-zero winding and even-odd fill rule
let shapeLayer = CAShapeLayer()

shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: 150.0, height: 150.0)
shapeLayer.lineWidth = 2.0
shapeLayer.fillColor = UIColor.pinkColor().CGColor
shapeLayer.fillRule = kCAFillRuleNonZero

let outerPath = UIBezierPath(rect: CGRectInset(shapeLayer.bounds, 20.0, 20.0))
let innerPath = UIBezierPath(rect: CGRectInset(shapeLayer.bounds, 50.0, 50.0))

let shapeLayerPath = UIBezierPath()
shapeLayerPath.appendPath(outerPath)
shapeLayerPath.appendPath(innerPath)

shapeLayer.path = shapeLayerPath.CGPath
Example shape layer using non-zero winding fill rule
let shapeLayer = CAShapeLayer()

shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: 150.0, height: 150.0)
shapeLayer.lineWidth = 2.0
shapeLayer.fillColor = UIColor.pinkColor().CGColor
shapeLayer.fillRule = kCAFillRuleEvenOdd

let outerPath = UIBezierPath(rect: CGRectInset(shapeLayer.bounds, 20.0, 20.0))
let innerPath = UIBezierPath(rect: CGRectInset(shapeLayer.bounds, 50.0, 50.0))

let shapeLayerPath = UIBezierPath()
shapeLayerPath.appendPath(outerPath)
shapeLayerPath.appendPath(innerPath)

shapeLayer.path = shapeLayerPath.CGPath
Example shape layer using even-odd winding fill rule

Line

CAShapeLayer has several properties related to how it draws lines: lineWidth, lineJoin, miterLimit, lineCap, lineDashPattern, and lineDashPhase. We’ll look at all of them.

Line Width

This one’s obvious: this determines the width of the line used to stroke the path.

Example paths with different stroke widths

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.

Line Join

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.

Example paths with different line join styles
let starShapeLayer1 = ...
starShapeLayer1.lineJoin = kCALineJoinMiter     // Miter, the default

let starShapeLayer2 = ...
starShapeLayer2.lineJoin = kCALineJoinRound     // Round

let starShapeLayer3 = ...
starShapeLayer3.lineJoin = kCALineJoinBevel     // Bevel
Example shape layer line join styles

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.

Three different paths, all with miter join styles but different miter limits
let starShapeLayer1 = ...
starShapeLayer1.lineWidth = 10.0
starShapeLayer1.miterLimit = 2.0

let starShapeLayer2 = ...
starShapeLayer2.lineWidth = 5.0
starShapeLayer2.miterLimit = 3.0

let starShapeLayer3 = ...
starShapeLayer3.lineWidth = 10.0
starShapeLayer3.miterLimit = 10.0
Example shape layer miter limits

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

Line cap determines how the end points of open paths are stroked: butt, round, or square.

Example paths with different line cap styles

(The thin, white lines are added for emphasis only and don’t have anything to do with how line caps render.)

let lineShapeLayer1 = ...
lineShapeLayer1.lineWidth = 20.0
lineShapeLayer1.lineCap = kCALineCapButt        // Butt, the default

let lineShapeLayer2 = ...
lineShapeLayer2.lineWidth = 20.0
lineShapeLayer2.lineCap = kCALineCapRound       // Round

let lineShapeLayer3 = ...
lineShapeLayer3.lineWidth = 20.0
lineShapeLayer3.lineCap = kCALineCapSquare      // Square
Example shape layers with different line cap styles

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.

Example paths with different line dash patterns
let circleShapeLayer1 = ...
circleShapeLayer1.lineWidth = 2.0
circleShapeLayer1.lineDashPattern = [10, 5, 20, 5]

let circleShapeLayer2 = ...
circleShapeLayer2.lineWidth = 2.0
circleShapeLayer2.lineDashPattern = [5, 3]

let circleShapeLayer3 = ...
circleShapeLayer3.lineWidth = 2.0
circleShapeLayer3.lineDashPattern = [1]

let circleShapeLayer4 = ...
circleShapeLayer4.lineWidth = 2.0
circleShapeLayer4.lineDashPattern = [47.12]
Example shape layer with different line dash patterns

Note circleShapeLayer3’s and 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.

Example paths with different line dash phases
let circleShapeLayer1 = ...
circleShapeLayer1.lineWidth = 2.0
circleShapeLayer1.lineDashPattern = [47.12]

let circleShapeLayer2 = ...
circleShapeLayer2.lineWidth = 2.0
circleShapeLayer2.lineDashPattern = [47.12]
circleShapeLayer2.lineDasePhase = 23.56

let circleShapeLayer3 = ...
circleShapeLayer3.lineWidth = 2.0
circleShapeLayer3.lineDashPattern = [47.12]
circleShapeLayer3.lineDasePhase = -23.56
Example shape layer with different line dash phases

Stroke

Lastly, shape layers have a few properties affecting the stroke: strokeColor, strokeStart, and strokeEnd.

Stroke Color

Stroke color should be very obvious: the color that’s used to stroke the path.

Example paths with different stroke colors

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.

let squareShapeLayer = ...
squareShapeLayer.lineWidth = 2.0
squareShapeLayer.strokeColor = UIColor.blueColor().CGColor

let circleShapeLayer = ...
circleShapeLayer.lineWidth = 2.0
circleShapeLayer.strokeColor = UIColor.yellowColor().CGColor

let patternImage = UIImage(named: "red-diagonal-stripe-pattern-image")

let starShapeLayer = ...
starShapeLayer.strokeColor = UIColor(patternImage: patternImage).CGColor
Example shape layers with different stroke colors

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.

Example paths with different stroke starts and ends

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.

let squareShapeLayer = ...
squareShapeLayer.lineWidth = 2.0
squareShapeLayer.strokeEnd = 0.62

let circleShapeLayer = ...
circleShapeLayer.lineWidth = 2.0
circleShapeLayer.strokeStart = 0.12
circleShapeLayer.strokeEnd = 0.88

let starShapeLayer = ...
starShapeLayer.lineWidth = 2.0
starShapeLayer.strokeEnd = 0.635
Example shape layers with different stroke colors

Summary

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.

  1. 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 shouldRasterize and rasterizationScale accordingly. Rasterization can affect performance, especially during animations, so use them judiciously.

  2. 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.

  3. In fact, the documentation makes a point to mention that CAShapeLayer favors speed over accuracy. So if your designs require absolute accuracy, you’re better off drawing with Core Graphics instead.

  4. UIBezierPath is just a wrapper around CGPath, though they are not toll-free bridged.

  5. Astute readers may point out that UIBezierPath itself 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, CAShapeLayer disregards these properties in favor of applying its own properties universally to the entire path.

  6. Keep in mind that fillColor, like most Core Animation properties, needs to be a Core Foundation type, i.e., CGColor, not UIColor.

  7. For this reason, I always thought UIColor should have been named UIFill, since a pattern image isn’t really a color. Ah well…

  8. This is contrary to how CALayer borders work: borders always draw starting at the bounds edges moving inward.

  9. 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.

  10. 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.

CALayer