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.

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. Most importantly, CAShapeLayer is, unsurprisingly, rendered entirely on the GPU, making it very fast. 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.

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 * . 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 = . 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 . move ( to : point ) } else { starPath . addLine ( to : point ) } } starPath . close () // Rotate the path so the star points up as expected var pathTransform = CGAffineTransform . identity pathTransform = pathTransform . translatedBy ( x : center . x , y : center . y ) pathTransform = pathTransform . rotated ( by : CGFloat ( -. pi / 2.0 )) pathTransform = pathTransform . translatedBy ( x : - center . x , y : - center . y ) starPath . apply ( 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 . append ( circlePath ) shapeLayerPath . append ( squarePath ) shapeLayerPath . append ( 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. 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 close() 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 move(to:) , 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 . move ( to : . zero ) openSquarePath . addLine ( to : CGPoint ( x : 0.0 , y : 120.0 )) openSquarePath . addLine ( to : CGPoint ( x : 120.0 , y : 120.0 )) openSquarePath . addLine ( to : 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 . 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 = . 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 . move ( to : point ) } else { starPath . addLine ( to : point ) } } // Rotate the path so the star points up as expected var pathTransform = CGAffineTransform . identity pathTransform = pathTransform . translatedBy ( x : center . x , y : center . y ) pathTransform = pathTransform . rotated ( by : CGFloat ( -. pi / 2.0 )) pathTransform = pathTransform . translatedBy ( x : - center . x , y : - center . y ) starPath . apply ( 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 fillColor 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. 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 . pink . cgColor shapeLayer . fillRule = kCAFillRuleNonZero let outerPath = UIBezierPath ( rect : shapeLayer . bounds . insetBy ( x : 20.0 , y : 20.0 )) let innerPath = UIBezierPath ( rect : shapeLayer . bounds . insetBy ( x : 50.0 , y : 50.0 )) let shapeLayerPath = UIBezierPath () shapeLayerPath . append ( outerPath ) shapeLayerPath . append ( 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 . pink . cgColor shapeLayer . fillRule = kCAFillRuleEvenOdd let outerPath = UIBezierPath ( rect : shapeLayer . bounds . insetBy ( x : 20.0 , y : 20.0 )) let innerPath = UIBezierPath ( rect : shapeLayer . bounds . insetBy ( x : 50.0 , y : 50.0 )) let shapeLayerPath = UIBezierPath () shapeLayerPath . append ( outerPath ) shapeLayerPath . append ( 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. 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 space—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 . blue . cgColor let circleShapeLayer = ... circleShapeLayer . lineWidth = 2.0 circleShapeLayer . strokeColor = UIColor . yellow . 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 CPU 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 a few examples of complex or interesting uses of shape layers, including some applications which may not be immediately obvious.

Please enable JavaScript to view the comments powered by Disqus.