This chapter has been revised for Early Release. It reflects iOS 14, Xcode 12, and Swift 5.3. But screenshots have not been retaken; they still show the Xcode 11 / iOS 13 interface.
The tale told in Chapters 1 and 2 of how a UIView draws itself is only half the story. A UIView has a partner called its layer, a CALayer. A UIView does not actually draw itself onto the screen; it draws itself into its layer, and it is the layer that is portrayed on the screen. As I’ve already mentioned, a view is not redrawn frequently; instead, its drawing is cached, and the cached version of the drawing (the bitmap backing store) is used where possible. The cached version is, in fact, the layer. What I spoke of in Chapter 2 as the view’s graphics context is actually the layer’s graphics context.
This might seem to be a mere implementation detail, but layers are important and interesting in their own right. To understand layers is to understand views more deeply; layers extend the power of views:
Layers have drawing-related properties beyond those of a UIView. Because a layer is the recipient and presenter of a view’s drawing, you can modify how a view is drawn on the screen by accessing the layer’s properties. By reaching down to the level of its layer, you can make a view do things you can’t do through UIView methods alone.
A UIView’s partner layer can contain additional layers. Since the purpose of layers is to draw, portraying visible material on the screen, this allows a UIView’s drawing to be composited of multiple distinct pieces. This can make drawing easier, with the constituents of a drawing being treated as objects.
Animation allows you to add clarity, emphasis, and just plain coolness to your interface. Layers are made to be animated; the “CA” in “CALayer” stands for “Core Animation.” Animation is the subject of Chapter 4.
Suppose we want to add a compass indicator to our app’s interface. Figure 3-1 portrays a simple version of such a compass. It takes advantage of the arrow that we drew in Chapter 2; the arrow is drawn into a layer of its own. The other parts of the compass are layers too: the circle is a layer, and each of the cardinal point letters is a layer. The drawing is easy to assemble in code (and later in this chapter, that’s exactly what we’ll do); even more intriguing, the pieces can be repositioned and animated separately, so it’s easy to rotate the arrow without moving the circle (and in Chapter 4, that’s exactly what we’ll do).
New in Xcode 12, the view debugger (“View Debugger”) can display layers. Choose Editor → Show Layers.
A UIView instance has an accompanying CALayer instance, accessible as the view’s layer
property. This layer has a special status: it is partnered with this view to embody all of the view’s drawing. The layer has no corresponding view
property, but the view is the layer’s delegate
(adopting CALayerDelegate). The documentation sometimes speaks of this layer as the view’s underlying layer.
Because every view has an underlying layer, there is a tight integration between the two. The layer portrays all the view’s drawing; if the view draws, it does so by contributing to the layer’s drawing. The view is the layer’s delegate. And the view’s properties are often merely a convenience for accessing the layer’s properties. When you set the view’s backgroundColor
, you are really setting the layer’s backgroundColor
, and if you set the layer’s backgroundColor
directly, the view’s backgroundColor
is set to match. Similarly, the view’s frame
is really the layer’s frame
and vice versa.
The view draws into its layer, and the layer caches that drawing; the layer can then be manipulated, changing the view’s appearance, without necessarily asking the view to redraw itself. This is a source of great efficiency in the drawing system. It also explains such phenomena as the content stretching that we encountered in the last section of Chapter 2: when the view’s bounds size changes, the drawing system, by default, simply stretches or repositions the cached layer image, until such time as the view is told to draw freshly, replacing the layer’s content.
By default, when a UIView is instantiated, its layer is a plain vanilla CALayer. But that might not be what you want. Suppose you have declared a UIView subclass, and you want your subclass’s underlying layer to be an instance of a CALayer subclass (built-in or your own). Then override your UIView subclass’s layerClass
class property to return that CALayer subclass.
That is how the compass in Figure 3-1 is created. We have a UIView subclass, CompassView, and a CALayer subclass, CompassLayer. Here is CompassView’s implementation:
class CompassView : UIView { override class var layerClass : AnyClass { return CompassLayer.self } }
When CompassView is instantiated, its underlying layer is a CompassLayer. In this example, there is no drawing in CompassView; its job — in this case, its only job — is to give CompassLayer a place in the visible interface, because a layer cannot appear without a view.
A layer can have sublayers, and a layer has at most one superlayer. We may say there is a tree of layers. This is similar and parallel to the tree of views (Chapter 1). In fact, so tight is the integration between a view and its underlying layer that these hierarchies are effectively the same hierarchy. Given a view and its underlying layer, that layer’s superlayer is the view’s superview’s underlying layer, and that layer has as sublayers all the underlying layers of all the view’s subviews. Indeed, because the layers are how the views actually get drawn, one might say that the view hierarchy really is a layer hierarchy (Figure 3-2).
At the same time, the layer hierarchy can go beyond the view hierarchy. A view has exactly one underlying layer, but a layer can have sublayers that are not the underlying layers of any view. So the hierarchy of layers that underlie views exactly matches the hierarchy of views, but the total layer tree may be a superset of that hierarchy. In Figure 3-3, we see the same view-and-layer hierarchy as in Figure 3-2, but two of the layers have additional sublayers that are theirs alone (that is, sublayers that are not any view’s underlying layers).
From a visual standpoint, there may be nothing to distinguish a hierarchy of views from a hierarchy of layers. In Chapter 1 we drew three overlapping rectangles using a hierarchy of views (Figure 1-1). This code gives exactly the same visible display by manipulating layers (Figure 3-4):
let lay1 = CALayer() lay1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1).cgColor lay1.frame = CGRect(113, 111, 132, 194) self.view.layer.addSublayer(lay1) let lay2 = CALayer() lay2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1).cgColor lay2.frame = CGRect(41, 56, 132, 194) lay1.addSublayer(lay2) let lay3 = CALayer() lay3.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1).cgColor lay3.frame = CGRect(43, 197, 160, 230) self.view.layer.addSublayer(lay3)
In Figure 3-4, Layer 1 and Layer 2 are siblings — they are sublayers of the main view’s underlying layer — while Layer 3 is a sublayer of Layer 2. That’s exactly parallel to the situation in Figure 1-1.
A view’s subview’s underlying layer is a sublayer of the view’s underlying layer. Therefore, it can be positioned anywhere in the drawing order among any other sublayers the view may have. The fact that a view can be interspersed among layers is surprising to beginners.
To illustrate, let’s construct Figure 3-4 again, but between adding lay2
and lay3
to the interface, we’ll add a subview:
// ... lay1.addSublayer(lay2) // Layer 3 let iv = UIImageView(image:UIImage(named:"smiley")) self.view.addSubview(iv) // Smiley view iv.frame.origin = CGPoint(180,180) let lay3 = CALayer() // Layer 1 // ...
The result is Figure 3-5. The smiley face (iv
) was added to the interface before Layer 1 (lay3
), so it appears behind it.
The smiley face is a view, whereas Layer 1 is just a layer; so they are not siblings as views, since Layer 1 is not a view. But the smiley face is both a view and a layer; as layers, the smiley face and Layer 1 are siblings, since they have the same superlayer, namely the main view’s layer.
As siblings, either one can be made to appear in front of the other.
Whether a layer displays regions of its sublayers that lie outside that layer’s own bounds depends upon the value of its masksToBounds
property. This is parallel to a view’s clipsToBounds
property, and indeed, for a layer that is a view’s underlying layer, they are the same thing. In
Figures 3-4 and 3-5,
the layers all have clipsToBounds
set to false
(the default); that’s why Layer 3 is visible beyond the bounds of Layer 2, which is its superlayer.
Like a UIView, a CALayer has an isHidden
property that can be set to take it and its sublayers out of the visible interface without actually removing it from its superlayer.
Layers come with a full set of methods for reading and manipulating the layer hierarchy, parallel to the methods for reading and manipulating the view hierarchy. A layer has a superlayer
property and a sublayers
property, along with these methods:
addSublayer(_:)
insertSublayer(_:at:)
insertSublayer(_:below:)
, insertSublayer(_:above:)
replaceSublayer(_:with:)
removeFromSuperlayer
Unlike a view’s subviews
property, a layer’s sublayers
property is writable. You can give a layer multiple sublayers in a single move, by assigning to its sublayers
property. To remove all of a layer’s sublayers, set its sublayers
property to nil
.
Although a layer’s sublayers have an order, reflected in the sublayers
order and regulated with the methods I’ve just mentioned, this is not necessarily the same as their back-to-front drawing order. By default, it is, but a layer also has a zPosition
property, a CGFloat, and this also determines drawing order. The rule is that all sublayers with the same zPosition
are drawn in the order they are listed among their sublayers
siblings, but lower zPosition
siblings are drawn before higher zPosition
siblings. (The default zPosition
is 0.0
.)
Sometimes, the zPosition
property is a more convenient way of dictating drawing order than sibling order is. If layers represent playing cards laid out in a solitaire game, it will likely be a lot easier and more flexible to determine how the cards overlap by setting their zPosition
than by rearranging their sibling order.
Moreover, a subview’s layer is itself just a layer, so you can rearrange the drawing order of subviews by setting the zPosition
of their underlying layers! In our code constructing Figure 3-5, if we assign the image view’s underlying layer a zPosition
of 1
, it is drawn in front of Layer 1:
self.view.addSubview(iv) iv.layer.zPosition = 1
Layer coordinate systems and positioning are similar to those of views. A layer’s own internal coordinate system is expressed by its bounds
, just like a view; its size is its bounds size, and its bounds origin is the internal coordinate at its top left.
A sublayer’s position within its superlayer is not described by its center
, like a view; a layer does not have a center
. Instead, a sublayer’s position within its superlayer is defined by a combination of two properties:
position
A point expressed in the superlayer’s coordinate system.
anchorPoint
Where the position
point is located, with respect to the layer’s own bounds. It is a CGPoint describing a fraction (or multiple) of the layer’s own bounds width and bounds height. (0.0,0.0)
is the top left of the layer’s bounds, and (1.0,1.0)
is the bottom right of the layer’s bounds.
Here’s an analogy; I didn’t make it up, but it’s pretty apt. Think of the sublayer as pinned to its superlayer; then you have to say both where the pin passes through the sublayer (the anchorPoint
) and where it passes through the superlayer (the position
).
If the anchorPoint
is (0.5,0.5)
(the default), the position
property works like a view’s center
property. A view’s center
is actually a special case of a layer’s position
. This is quite typical of the relationship between view properties and layer properties; the view properties are often a simpler — but less powerful — version of the layer properties.
A layer’s position
and anchorPoint
are orthogonal (independent); changing one does not change the other. Therefore, changing either of them without changing the other changes where the layer is drawn within its superlayer.
In Figure 3-1, the most important point in the circle is its center; all the other objects need to be positioned with respect to it. Therefore, they all have the same position
: the center of the circle. But they differ in their anchorPoint
. The arrow’s anchorPoint
is (0.5,0.8)
, the middle of the shaft, near the tail. But the anchorPoint
of a cardinal point letter is (0.5,3.0)
, well outside the letter’s bounds, so as to place the letter near the edge of the circle.
A layer’s frame
is a purely derived property. When you get the frame
, it is calculated from the bounds size along with the position
and anchorPoint
. When you set the frame
, you set the bounds size and position
. In general, you should regard the frame
as a convenient façade and no more. Nevertheless, it is convenient! To position a sublayer so that it exactly overlaps its superlayer, you can just set the sublayer’s frame
to the superlayer’s bounds
.
A layer created in code (as opposed to a view’s underlying layer) has a frame
and bounds
of CGRect.zero
and will not be visible on the screen even when you add it to a superlayer that is on the screen. Be sure to give your layer a nonzero width and height if you want to be able to see it! Creating a layer and adding it to a superlayer and then wondering why it isn’t appearing in the interface is a common beginner error.
CALayer instance methods are provided for converting between the coordinate systems of layers within the same layer hierarchy; the first parameter can be a CGPoint or a CGRect, and the second parameter is another CALayer:
convert(_:from:)
convert(_:to:)
If you’re going to be moving a layer’s bounds origin as a way of repositioning its sublayers en masse, you might like to make the layer a CAScrollLayer, a CALayer subclass that provides convenience methods for this sort of thing. (Despite the name, a CAScrollLayer provides no scrolling interface; the user can’t scroll it by dragging.) By default, a CAScrollLayer’s masksToBounds
property is true
, so that the CAScrollLayer acts like a window through which you see only what is within its bounds. (You can set its masksToBounds
to false
, but this would be an odd thing to do, as it somewhat defeats the purpose.)
To move the CAScrollLayer’s bounds, you can talk either to it or to a sublayer (at any depth):
Call this CAScrollLayer method:
scroll(to:)
If the parameter is a CGPoint, changes the CAScrollLayer’s bounds origin to that point. If the parameter is a CGRect, changes the CAScrollLayer’s bounds origin minimally so that the given portion of the bounds rect is visible.
Call these CALayer methods on sublayers of the CAScrollLayer:
scroll(_:)
Changes the CAScrollLayer’s bounds origin so that the given point of the sublayer is at the top left of the CAScrollLayer.
scrollRectToVisible(_:)
Changes the CAScrollLayer’s bounds origin so that the given rect of the sublayer’s bounds is within the CAScrollLayer’s bounds area. You can also ask the sublayer for its visibleRect
, the part of this sublayer now within the CAScrollLayer’s bounds.
A CALayer’s delegate
property is settable (to an instance of any class adopting CALayerDelegate). If a layer is not the underlying layer of a view, it can be useful to give it a delegate; you might do this in order to give another object a hand in the layer’s layout or its drawing, both of which I’ll discuss later. A layer’s delegate can also participate in certain aspects of animation, as I’ll explain in Chapter 4.
But be careful! Do not, under any circumstances, set the delegate
of a layer that is the underlying layer of a UIView; and do not make a UIView the delegate
of any layer other than its underlying layer. A UIView must be the delegate of its underlying layer; moreover, it must not be the delegate of any other layer. Don’t do anything to mess this up. If you do, extremely bad things will happen; for instance, drawing will stop working correctly.
The view hierarchy is actually a layer hierarchy (Figure 3-2). The positioning of a view within its superview is actually the positioning of its layer within its superlayer (the superview’s layer). A view can be repositioned and resized automatically in accordance with its autoresizingMask
or through autolayout based on its constraints. So there is automatic layout of layers if they are the underlying layers of views. Otherwise, there is no automatic layout of layers in iOS; the only option for layout of layers that are not the underlying layers of views is manual layout that you perform in code.
When a layer needs layout, either because its bounds have changed or because you called setNeedsLayout
, you can respond in either of two ways:
The layer’s layoutSublayers
method is called; to respond, override layoutSublayers
in your CALayer subclass.
Alternatively, implement layoutSublayers(of:)
in the layer’s delegate. (Remember, if the layer is a view’s underlying layer, the view is its delegate.)
For your layer to do effective manual layout of its sublayers, you’ll probably need a way to identify or refer to the sublayers. There is no layer equivalent of viewWithTag(_:)
, so such identification and reference is entirely up to you. A CALayer does have a name
property that you might misuse for your own purposes. Key–value coding can also be helpful here; layers implement key–value coding in a special way, discussed at the end of this chapter.
For a view’s underlying layer, layoutSublayers
or layoutSublayers(of:)
is called after the view’s layoutSubviews
. Under autolayout, you must call super
or else autolayout will break. Moreover, these methods may be called more than once during the course of autolayout; if you’re looking for an automatically generated signal that it’s time to do manual layout of sublayers, a view layout event might be a better choice (see “Layout Events”).
The simplest way to make something appear in a layer is through its contents
property. This is parallel to the image
in a UIImageView (Chapter 2). It is expected to be a CGImage (or nil
, signifying no image). Here’s how we might modify the code that generated Figure 3-5 in such a way as to generate the smiley face as a layer rather than a view:
let lay4 = CALayer() let im = UIImage(named:"smiley")! lay4.frame = CGRect(origin:CGPoint(180,180), size:im.size) lay4.contents = im.cgImage self.view.layer.addSublayer(lay4)
Unfortunately, the CALayer contents
property is typed as Any (wrapped in an Optional). That means you can assign anything to it. Setting a layer’s contents
to a UIImage, rather than a CGImage, will fail silently — the image doesn’t appear, but there is no error either. This is absolutely maddening, and I wish I had a nickel for every time I’ve done it and then wasted hours figuring out why my layer isn’t appearing.
There are also four methods that can be implemented to provide or draw a layer’s content on demand, similar to a UIView’s draw(_:)
. A layer is very conservative about calling these methods (and you must not call any of them directly). When a layer does call these methods, I will say that the layer redisplays itself. Here is how a layer can be caused to redisplay itself:
If the layer’s needsDisplayOnBoundsChange
property is false
(the default), then the only way to cause the layer to redisplay itself is by calling setNeedsDisplay
(or setNeedsDisplay(_:)
, specifying a CGRect). Even this might not cause the layer to redisplay itself right away; if that’s crucial, then you will also call displayIfNeeded
.
If the layer’s needsDisplayOnBoundsChange
property is true
, then the layer will also redisplay itself when the layer’s bounds change (rather like a view’s .redraw
content mode).
Here are the four methods that can be called when a layer redisplays itself; pick one to implement (don’t try to combine them, you’ll just confuse things):
display
in a subclassYour CALayer subclass can override display
. There’s no graphics context at this point, so display
is pretty much limited to setting the contents
image.
display(_:)
in the delegateYou can set the CALayer’s delegate
property and implement display(_:)
in the delegate. As with CALayer’s display
, there’s no graphics context, so you’ll just be setting the contents
image.
draw(in:)
in a subclassYour CALayer subclass can override draw(in:)
. The parameter is a graphics context into which you can draw directly; it is not automatically made the current context.
draw(_:in:)
in the delegateYou can set the CALayer’s delegate
property and implement draw(_:in:)
in the delegate. The second parameter is a graphics context into which you can draw directly; it is not automatically made the current context.
Assigning a layer a contents
image and drawing directly into the layer are, in effect, mutually exclusive:
If a layer’s contents
is assigned an image, this image is shown immediately and replaces whatever drawing may have been displayed in the layer.
If a layer redisplays itself and draw(in:)
or draw(_:in:)
draws into the layer, the drawing replaces whatever image may have been displayed in the layer.
If a layer redisplays itself and none of the four methods provides any content, the layer will be empty.
If a layer is a view’s underlying layer, you usually won’t use any of the four methods to draw into the layer: you’ll use the view’s draw(_:)
. But you can use these methods if you really want to. In that case, you will probably want to implement draw(_:)
anyway, leaving that implementation empty. The reason is that this causes the layer to redisplay itself at appropriate moments. When a view is sent setNeedsDisplay
— including when the view first appears — the view’s underlying layer is also sent setNeedsDisplay
, unless the view has no draw(_:)
implementation (because in that case, it is assumed that the view never needs redrawing). If you’re drawing a view entirely by drawing to its underlying layer directly, and if you want the underlying layer to be redisplayed automatically when the view is told to redraw itself, you should implement draw(_:)
to do nothing. (This technique has no effect on sublayers of the underlying layer.) These are legitimate (but unusual) techniques for drawing into a view:
The view subclass implements an empty draw(_:)
, along with either displayLayer:
or draw(_:in:)
.
The view subclass implements an empty draw(_:)
plus layerClass
, to give the view a custom layer subclass — and the custom layer subclass implements either display
or draw(in:)
.
A layer has a scale, its contentsScale
, which maps point distances in the layer’s graphics context to pixel distances on the device. A layer that’s managed by Cocoa, if it has contents, will adjust its contentsScale
automatically as needed; if a view implements draw(_:)
, then on a device with a double-resolution screen its underlying layer is assigned a contentsScale
of 2. But a layer that you are creating and managing yourself has no such automatic behavior; it’s up to you, if you plan to draw into the layer, to set its contentsScale
appropriately. Content drawn into a layer with a contentsScale
of 1 may appear pixellated or fuzzy on a high-resolution screen. And when you’re starting with a UIImage and assigning its CGImage as a layer’s contents
, if there’s a mismatch between the UIImage’s scale
and the layer’s contentsScale
, then the image may be displayed at the wrong size.
Three further layer properties strongly affect what the layer displays:
backgroundColor
Equivalent to a view’s backgroundColor
(and if this layer is a view’s underlying layer, it is the view’s backgroundColor
). Changing the backgroundColor
takes effect immediately. Think of the backgroundColor
as separate from the layer’s own drawing, and as painted behind the layer’s own drawing.
opacity
Affects the overall apparent transparency of the layer. It is equivalent to a view’s alpha
(and if this layer is a view’s underlying layer, it is the view’s alpha
). It affects the apparent transparency of the layer’s sublayers as well. It affects the apparent transparency of the background color and the apparent transparency of the layer’s content separately (just as with a view’s alpha
). Changing the opacity
property takes effect immediately.
isOpaque
Determines whether the layer’s graphics context is opaque. An opaque graphics context is black; you can draw on top of that blackness, but the blackness is still there. A nonopaque graphics context is clear; where no drawing is, it is completely transparent.
Changing the isOpaque
property has no effect until the layer redisplays itself. A view’s underlying layer’s isOpaque
property is independent of the view’s isOpaque
property; they are unrelated and do entirely different things. But if the view implements draw(_:)
, then setting the view’s backgroundColor
changes the layer’s isOpaque
! The latter becomes true
if the new background color is opaque (alpha component of 1
), and false
otherwise. That’s the reason behind the strange behavior of CGContext’s clear(_:)
method, described in Chapter 2.
When drawing directly into a layer, the behavior of clear(_:)
differs from what was described in Chapter 2 for drawing into a view: instead of punching a hole through the background color, it effectively paints with the layer’s background color. This can have curious side effects, and I regard it as deeply weird.
A layer’s content is stored (cached) as a bitmap which is then treated like an image:
If the content came from setting the layer’s contents
property to an image, the cached content is that image; its size is the point size of the CGImage we started with.
If the content came from drawing directly into the layer’s graphics context, the cached content is the layer’s entire graphics context; its size is the point size of the layer itself at the time the drawing was performed.
The layer’s content is drawn in relation to the layer’s bounds in accordance with various layer properties, which cause the cached content to be resized, repositioned, cropped, and so on, as it is displayed. The properties are:
contentsGravity
This property (CALayerContentsGravity) is parallel to a UIView’s contentMode
property, and describes how the content should be positioned or stretched in relation to the bounds. .center
means the content is centered in the bounds without resizing; .resize
(the default) means the content is sized to fit the bounds, even if this means distorting its aspect; and so forth.
For historical reasons, the terms bottom
and top
in the names of the contentsGravity
settings have the opposite of their expected meanings. Reverse them in your mind: when you mean the top, say .bottom
; when you mean the top left, say .bottomLeft
; and so on.
contentsRect
A CGRect expressing the proportion of the content that is to be displayed. The default is (0.0,0.0,1.0,1.0)
, meaning the entire content is displayed. The specified part of the content is sized and positioned in relation to the bounds in accordance with the contentsGravity
. By setting the contentsRect
, you can scale up part of the content to fill the bounds, or slide part of a larger image into view without redrawing or changing the contents
image.
You can also use the contentsRect
to scale down the content, by specifying a larger contentsRect
such as (-0.5,-0.5,1.5,1.5)
; but any content pixels that touch the edge of the contentsRect
will then be extended outward to the edge of the layer (to prevent this, make sure that the outermost pixels of the content are all empty).
contentsCenter
A CGRect, structured like contentsRect
, expressing the central region of nine rectangular regions of the contentsRect
that are variously allowed to stretch if the contentsGravity
calls for stretching. The central region (the actual value of the contentsCenter
) stretches in both directions. Of the other eight regions (inferred from the value you provide), the four corner regions don’t stretch, and the four side regions stretch in one direction. (This should remind you of how a resizable image stretches; see Chapter 2.)
If a layer’s content comes from drawing directly into its graphics context, then the layer’s contentsGravity
, of itself, has no effect, because the size of the graphics context, by definition, fits the size of the layer exactly; there is nothing to stretch or reposition. But the contentsGravity
will have an effect on such a layer if its contentsRect
is not (0.0,0.0,1.0,1.0)
, because now we’re specifying a rectangle of some other size; the contentsGravity
describes how to fit that rectangle into the layer.
Again, if a layer’s content comes from drawing directly into its graphics context, then when the layer is resized, if the layer is asked to display itself again, the drawing is performed again, and once more the layer’s content fits the size of the layer exactly. But if the layer’s bounds are resized when needsDisplayOnBoundsChange
is false
, then the layer does not redisplay itself, so its cached content no longer fits the layer, and the contentsGravity
matters.
By a judicious combination of settings, you can get the layer to perform some clever drawing for you that might be difficult to perform directly. Figure 3-6 shows the result of the following settings:
arrow.needsDisplayOnBoundsChange = false arrow.contentsCenter = CGRect(0.0, 0.4, 1.0, 0.6) arrow.contentsGravity = .resizeAspect arrow.bounds = arrow.bounds.insetBy(dx: -20, dy: -20)
Because needsDisplayOnBoundsChange
is false
, the content is not redisplayed when the arrow’s bounds are increased; instead, the cached content is used. The contentsGravity
setting tells us to resize proportionally; therefore, the arrow is both longer and wider than in Figure 3-1, but not in such a way as to distort its proportions. Notice that although the triangular arrowhead is wider, it is not longer; the increase in length is due entirely to the stretching of the arrow’s shaft. That’s because the contentsCenter
region is within the shaft.
A layer’s masksToBounds
property has the same effect on its content that it has on its sublayers. If it is false
, the whole content is displayed, even if that content (after taking account of the contentsGravity
and contentsRect
) is larger then the layer. If it is true
, only the part of the content within the layer’s bounds will be displayed.
The value of a layer’s bounds origin does not affect where its content is drawn. It affects only where its sublayers are drawn.
A few built-in CALayer subclasses provide some basic but helpful self-drawing ability:
A CATextLayer has a string
property, which can be an NSString or NSAttributedString (Chapter 11), along with other text formatting properties, somewhat like a simplified UILabel; it draws its string
. The default text color, the foregroundColor
property, is white, which is unlikely to be what you want. The text is different from the contents
and is mutually exclusive with it: either the contents image or the text will be drawn, but not both, so in general you should not give a CATextLayer any contents image. In
Figures 3-1 and 3-6,
the cardinal point letters are CATextLayer instances.
A CAShapeLayer has a path
property, which is a CGPath. It fills or strokes this path, or both, depending on its fillColor
and strokeColor
values, and displays the result; the default is a fillColor
of black and no strokeColor
. It has properties for line thickness, dash style, end-cap style, and join style, similar to a graphics context; it also has the remarkable ability to draw only part of its path (strokeStart
and strokeEnd
), making it very easy to draw things like an arc of an ellipse. A CAShapeLayer may also have contents
; the shape is displayed on top of the contents image, but there is no property permitting you to specify a compositing mode. In
Figures 3-1 and 3-6,
the background circle is a CAShapeLayer instance, stroked with gray and filled with a lighter, slightly transparent gray.
A CAGradientLayer covers its background with a gradient, whose type (CAGradientLayerType) can be .axial
(linear), .radial
, or .conic
(starting in iOS 12). It’s an easy way to draw a gradient in your interface. The gradient is defined much as in the Core Graphics gradient example in Chapter 2, an array of locations and an array of corresponding colors, along with a start and end point. To clip the gradient’s shape, you can add a mask to the CAGradientLayer (masks are discussed later in this chapter). A CAGradientLayer’s contents
are not displayed. Figure 3-7 shows our compass drawn with a linear CAGradientLayer behind it.
The way a layer is drawn on the screen can be modified though a transform. This is not surprising, because a view can have a transform (see Chapter 1), and a view is drawn on the screen by its layer. You can treat a layer’s transform as two-dimensional or as three-dimensional; with some additional preparation, you can even make a three-dimensional transform look three-dimensional (in a cartoony way).
In the simplest case, when a transform is two-dimensional, you can access a layer’s transform through the affineTransform
method (and the corresponding setter, setAffineTransform(_:)
). The value is a CGAffineTransform, familiar from
Chapters 1 and 2.
A layer’s affineTransform
is analogous to a view’s transform
. For details, see “Transform”. The chief difference is that a view’s transform
is applied around its center
, whereas a layer’s transform is applied around the anchorPoint
, which might not be at its center. (So the anchorPoint
has a second purpose that I didn’t tell you about when discussing it earlier.)
You now know everything needed to understand the code that generated Figure 3-7. In this code, self
is the CompassLayer; it does no drawing of its own, but merely assembles and configures its sublayers. The four cardinal point letters are each drawn by a CATextLayer; they are drawn at the same coordinates, but they have different rotation transforms, and are anchored so that their rotation is centered at the center of the circle. For the arrow, CompassLayer adopts CALayerDelegate, makes itself the arrow layer’s delegate, and calls setNeedsDisplay
on the arrow layer; this causes draw(_:in:)
to be called in CompassLayer (that code is just the same code we developed for drawing the arrow in Chapter 2, and is not repeated here). The arrow layer is positioned by an anchorPoint
pinning its tail to the center of the circle, and rotated around that pin by a transform:
// the gradient let g = CAGradientLayer() g.contentsScale = UIScreen.main.scale g.frame = self.bounds g.colors = [ UIColor.black.cgColor, UIColor.red.cgColor ] g.locations = [0.0,1.0] self.addSublayer(g) // the circle let circle = CAShapeLayer() circle.contentsScale = UIScreen.main.scale circle.lineWidth = 2.0 circle.fillColor = UIColor(red:0.9, green:0.95, blue:0.93, alpha:0.9).cgColor circle.strokeColor = UIColor.gray.cgColor let p = CGMutablePath() p.addEllipse(in: self.bounds.insetBy(dx: 3, dy: 3)) circle.path = p self.addSublayer(circle) circle.bounds = self.bounds circle.position = self.bounds.center // the four cardinal points let pts = "NESW" for (ix,c) in pts.enumerated() { let t = CATextLayer() t.contentsScale = UIScreen.main.scale t.string = String(c) t.bounds = CGRect(0,0,40,40) t.position = circle.bounds.center let vert = circle.bounds.midY / t.bounds.height t.anchorPoint = CGPoint(0.5, vert) t.alignmentMode = .center t.foregroundColor = UIColor.black.cgColor t.setAffineTransform( CGAffineTransform(rotationAngle:CGFloat(ix) * .pi/2.0)) circle.addSublayer(t) } // the arrow let arrow = CALayer() arrow.contentsScale = UIScreen.main.scale arrow.bounds = CGRect(0, 0, 40, 100) arrow.position = self.bounds.center arrow.anchorPoint = CGPoint(0.5, 0.8) arrow.delegate = self // we will draw the arrow in the delegate method arrow.setAffineTransform(CGAffineTransform(rotationAngle:.pi/5.0)) self.addSublayer(arrow) arrow.setNeedsDisplay() // draw, please
A layer’s affineTransform
is merely a façade for accessing its transform
. A layer’s transform
is a three-dimensional transform, a CATransform3D; when the layer is a view’s underlying layer, this property is exposed at the level of the view through the view’s transform3D
. For details, see “Transform3D”. The chief difference is that a view’s transform3D
takes place around the view’s center
, whereas a layer’s three-dimensional transform
takes place around a three-dimensional extension of the anchorPoint
, whose z-component is supplied by the anchorPointZ
property. Most of the time, you’ll probably leave the anchorPointZ
at its default of 0.0
.
The following rotation flips a layer around its y-axis:
someLayer.transform = CATransform3DMakeRotation(.pi, 0, 1, 0)
What will we see after we do that? By default, the layer is considered double-sided, so when it is flipped to show its “back,” what’s drawn is an appropriately reversed version of the content of the layer (along with its sublayers, which by default are still drawn in front of the layer, but reversed and positioned in accordance with the layer’s transformed coordinate system). But if the layer’s isDoubleSided
property is false
, then when it is flipped to show its “back,” the layer disappears (along with its sublayers); its “back” is transparent and empty.
Layers do not magically give you realistic three-dimensional rendering — for that you would use Metal, which is beyond the scope of this discussion. Layers are two-dimensional objects, and they are designed for speed and simplicity. But read on, because I’m about to talk about adding an illusion of depth.
There are two ways to place layers at different nominal depths with respect to their siblings:
Through the z-component of their position
, which is the zPosition
property. (So the zPosition
has a second purpose that I didn’t tell you about earlier.)
By applying a transform that translates the layer’s position in the z-direction.
These two values, the z-component of a layer’s position and the z-component of its translation transform, are related; in some sense, the zPosition
is a shorthand for a translation transform in the z-direction. (If you provide both a zPosition
and a z-direction translation, you can rapidly confuse yourself.)
In the real world, changing an object’s zPosition
would make it appear larger or smaller, as it is positioned closer or further away; but this, by default, is not the case in the world of layer drawing. There is no attempt to portray perspective; the layer planes are drawn at their actual size and flattened onto one another, with no illusion of distance. (This is called orthographic projection, and is the way blueprints are often drawn to display an object from one side.) If we want to portray a visual sense of depth using layers, we’re going to need some additional techniques, as I shall now explain.
Here’s a widely used trick for adding a sense of perspective to the way layers are drawn: make them sublayers of a layer whose sublayerTransform
property maps all points onto a “distant” plane. (This is probably just about the only thing the sublayerTransform
property is ever used for.) Combined with orthographic projection, the effect is to apply one-point perspective to the drawing, so that things get perceptibly smaller in the negative z-direction.
Let’s try applying a sort of “page-turn” rotation to our compass: we’ll anchor it at its right side and then rotate it around the y-axis. Here, the sublayer we’re rotating (accessed through a property, rotationLayer
) is the gradient layer, and the circle and arrow are its sublayers so that they rotate with it:
self.rotationLayer.anchorPoint = CGPoint(1,0.5) self.rotationLayer.position = CGPoint(self.bounds.maxX, self.bounds.midY) self.rotationLayer.transform = CATransform3DMakeRotation(.pi/4.0, 0, 1, 0)
The results are disappointing (Figure 3-8); the compass looks more squashed than rotated. But now we’ll also apply the distance-mapping transform. The superlayer here is self
:
var transform = CATransform3DIdentity transform.m34 = -1.0/1000.0 self.sublayerTransform = transform
The results (shown in Figure 3-9) are better, and you can experiment with values to replace 1000.0
; for an even more exaggerated effect, try 500.0
. Also, the zPosition
of the rotationLayer
will now affect how large it is.
Another way to draw layers with depth is to use CATransformLayer. This CALayer subclass doesn’t do any drawing of its own; it is intended solely as a host for other layers. It has the remarkable feature that you can apply a transform to it and it will maintain the depth relationships among its own sublayers. In this example, how things look depends on whether or not lay1
is a CATransformLayer:
let lay2 = CALayer() lay2.frame = f // some CGRect lay2.backgroundColor = UIColor.blue.cgColor lay1.addSublayer(lay2) let lay3 = CALayer() lay3.frame = f.offsetBy(dx: 20, dy: 30) lay3.backgroundColor = UIColor.green.cgColor lay3.zPosition = 10 lay1.addSublayer(lay3) lay1.transform = CATransform3DMakeRotation(.pi, 0, 1, 0)
In that code, the superlayer lay1
has two sublayers, lay2
and lay3
. The sublayers are added in that order, so lay3
is drawn in front of lay2
. Then lay1
is flipped like a page being turned by setting its transform
:
If lay1
is a normal CALayer, the sublayer drawing order doesn’t change; lay3
is still drawn in front of lay2
, even after the transform is applied.
If lay1
is a CATransformLayer, lay3
is drawn behind lay2
after the transform; they are both sublayers of lay1
, so their depth relationship is maintained.
Figure 3-10 shows our page-turn rotation yet again, still with the sublayerTransform
applied to self
, but this time the only sublayer of self
is a CATransformLayer:
var transform = CATransform3DIdentity transform.m34 = -1.0/1000.0 self.sublayerTransform = transform let master = CATransformLayer() master.frame = self.bounds self.addSublayer(master) self.rotationLayer = master
The CATransformLayer, to which the page-turn transform is applied, holds the gradient layer, the circle layer, and the arrow layer. Those three layers are at different depths (using different zPosition
settings), and I’ve tried to emphasize the arrow’s separation from the circle by adding a shadow (discussed in the next section):
circle.zPosition = 10 arrow.shadowOpacity = 1.0 arrow.shadowRadius = 10 arrow.zPosition = 20
You can see from its apparent offset that the circle layer floats in front of the gradient layer, but I wish you could see this page-turn as an animation, which makes the circle jump right out from the gradient as the rotation proceeds.
Even more remarkable, I’ve added a little white peg sticking through the arrow and running into the circle! It is a CAShapeLayer, rotated to be perpendicular to the CATransformLayer (I’ll explain the rotation code later in this chapter):
let peg = CAShapeLayer() peg.contentsScale = UIScreen.main.scale peg.bounds = CGRect(0,0,3.5,50) let p2 = CGMutablePath() p2.addRect(peg.bounds) peg.path = p2 peg.fillColor = UIColor(red:1.0, green:0.95, blue:1.0, alpha:0.95).cgColor peg.anchorPoint = CGPoint(0.5,0.5) peg.position = master.bounds.center master.addSublayer(peg) peg.setValue(Float.pi/2, forKeyPath:"transform.rotation.x") peg.setValue(Float.pi/2, forKeyPath:"transform.rotation.z") peg.zPosition = 15
In that code, the peg runs straight out of the circle toward the viewer, so it is initially seen end-on, and because a layer has no thickness, it is invisible. But as the CATransformLayer pivots in our page-turn rotation, the peg maintains its orientation relative to the circle, and comes into view. In effect, the drawing portrays a 3D model constructed entirely out of layers!
There is, I think, a slight additional gain in realism if the same sublayerTransform
is applied also to the CATransformLayer, but I have not done so here.
A CALayer has many additional properties that affect details of how it is drawn. Since these drawing details can be applied to a UIView’s underlying layer, they are effectively view features as well.
A CALayer can have a shadow, defined by its shadowColor
, shadowOpacity
, shadowRadius
, and shadowOffset
properties. To make the layer draw a shadow, set the shadowOpacity
to a nonzero value. The shadow is normally based on the shape of the layer’s nontransparent region, but deriving this shape can be calculation-intensive (so much so that in early versions of iOS, layer shadows weren’t implemented). You can vastly improve performance by defining the shape yourself as a CGPath and assigning it to the layer’s shadowPath
property.
If a layer’s masksToBounds
is true
, no part of its shadow lying outside its bounds is drawn. (This includes the underlying layer of a view whose clipsToBounds
is true
.) Wondering why the shadow isn’t appearing for a layer that masks to its bounds is a common beginner mistake.
This in turn poses a puzzle. Suppose we have a view for which clipsToBounds
must be true
, but we want that view to cast a shadow anyway. How can we do that? Figure 3-11 shows an example. The kitten image is shown in an image view whose contentMode
is .scaleAspectFill
. The original kitten image is rectangular, not square; the scaled image is therefore larger than the image view. But we don’t want the excess part of the image to be visible, so the image view’s clipsToBounds
is true
. Yet we also want the image view to cast a shadow. How can that be done?
It can’t be done. Figure 3-11 may look like that’s what it’s doing, but it isn’t. It’s a trick. There’s another view that you don’t see. It has the same frame
as the image view, its background color is black, and it’s behind the image view. Its clipsToBounds
is false
, and its layer
has a shadow.
A CALayer can have a border (borderWidth
, borderColor
); the borderWidth
is drawn inward from the bounds, potentially covering some of the content unless you compensate.
A CALayer’s corners can be rounded, effectively bounding the layer with a rounded rectangle, by giving it a cornerRadius
greater than zero. If the layer has a border, the border has rounded corners too. If the layer has a backgroundColor
, that background is clipped to the shape of the rounded rectangle. If the layer’s masksToBounds
is true
, the layer’s content and its sublayers are clipped by the rounded corners.
Starting in iOS 11, you can round individual corners of a CALayer rather than having to round all four corners at once. To do so, set the layer’s maskedCorners
property to a CACornerMask, a bitmask whose values have these dreadful names:
layerMinXMinYCorner
layerMaxXMinYCorner
layerMinXMaxYCorner
layerMaxXMaxYCorner
Even if you set the maskedCorners
, you won’t see any corner rounding unless you also set the cornerRadius
to a nonzero number.
A CALayer can have a mask
. This is itself a layer, whose content must be provided somehow. The transparency of the mask’s content in a particular spot becomes (all other things being equal) the transparency of the layer at that spot. The mask’s colors (hues) are irrelevant; only transparency matters. To position the mask, pretend it’s a sublayer.
Figure 3-12 shows our arrow layer, with the gray circle layer behind it, and a mask applied to the arrow layer. The mask is silly, but it illustrates very well how masks work: it’s an ellipse, with an opaque fill and a thick, semitransparent stroke. Here’s the code that generates and applies the mask:
let mask = CAShapeLayer() mask.frame = arrow.bounds let path = CGMutablePath() path.addEllipse(in: mask.bounds.insetBy(dx: 10, dy: 10)) mask.strokeColor = UIColor(white:0.0, alpha:0.5).cgColor mask.lineWidth = 20 mask.path = path arrow.mask = mask
Using a mask, we can do more generally what the cornerRadius
and masksToBounds
properties do. Here’s a utility method that generates a CALayer suitable for use as a rounded rectangle mask
:
func mask(size sz:CGSize, roundingCorners rad:CGFloat) -> CALayer { let rect = CGRect(origin:.zero, size:sz) let r = UIGraphicsImageRenderer(bounds:rect) let im = r.image { ctx in let con = ctx.cgContext con.setFillColor(UIColor(white:0, alpha:0).cgColor) con.fill(rect) con.setFillColor(UIColor(white:0, alpha:1).cgColor) let p = UIBezierPath(roundedRect:rect, cornerRadius:rad) p.fill() } let mask = CALayer() mask.frame = rect mask.contents = im.cgImage return mask }
The CALayer returned from that method can be placed as a mask anywhere in a layer by adjusting its frame origin and assigning it as the layer’s mask
. The result is that all of that layer’s content drawing and its sublayers (including, if this layer is a view’s underlying layer, the view’s subviews) are clipped to the rounded rectangle shape; everything outside that shape is not drawn.
A mask drawing can have values between opaque and transparent, and can mask to any shape. The transparent region doesn’t have to be on the outside of the mask; you can use a mask that’s opaque on the outside and transparent on the inside to “punch a hole” in a layer (or a view).
A mask is like a sublayer, in that there is no built-in mechanism for automatically resizing the mask as the layer is resized. If you don’t resize the mask when the layer is resized, the mask won’t be resized. A common beginner mistake is to apply a mask to a view’s underlying layer before the view has been fully laid out; when the view is laid out, its size changes, but the mask’s size doesn’t, and now the mask doesn’t “fit.”
Alternatively, you can apply a mask as a view directly to another view through the view’s mask
property, rather than having to drop down to the level of layers. This is not functionally distinct from applying the mask view’s layer to the view’s layer — under the hood, in fact, it is applying the mask view’s layer to the view’s layer. Using a mask view does nothing directly to help with the problem of resizing the mask when the view’s size changes; a mask view isn’t a subview, so it is not subject to autoresizing or autolayout. On the other hand, if you resize a mask view manually, you can do so using view properties; that’s very convenient if you’re already resizing the view itself manually (such as when using view property animation, as discussed in the next chapter).
By now, you’re probably envisioning all sorts of compositing fun, with layers masking sublayers and laid semitransparently over other layers. There’s nothing wrong with that, but when an iOS device is asked to shift its drawing from place to place, the movement may stutter because the device lacks the necessary computing power to composite repeatedly and rapidly. This sort of issue is likely to emerge particularly when your code performs an animation (Chapter 4) or when the user is able to animate drawing through touch, as when scrolling a table view (Chapter 8).
You may be able to detect these problems by eye, and you can quantify them on a device by using the Core Animation template in Instruments, which shows the frame rate achieved during animation. New in Xcode 12, animation “hitches” (as Apple now calls them) can be measured with XCTMetrics during performance testing. Also, the Simulator’s Debug menu lets you summon colored overlays that provide clues as to possible sources of inefficient drawing that can lead to such problems; when running on a device, similar overlays are available by choosing from the hierarchical menu under Debug → View Debugging → Rendering.
Tricks like shadows and rounded corners and masks may be easy and fun, but in general, opaque drawing is most efficient. (Nonopaque drawing is what the Simulator marks when you check Debug → Color Blended Layers.) You may think that for some particular use case you have to do nonopaque drawing, but think again, because you might be wrong about that. If a layer will always be shown over a background consisting of a single color, you can give the layer its own background of that same color; when additional layer content is supplied, the visual effect will be the same as if that additional layer content were composited over a transparent background.
Another way to gain some efficiency is by “freezing” the entirety of the layer’s drawing as a bitmap. In effect, you’re drawing everything in the layer to a secondary cache and using the cache to draw to the screen. Copying from a cache is less efficient than drawing directly to the screen, but this inefficiency may be compensated for, if there’s a deep or complex layer tree, by not having to composite that tree every time we render. To do this, set the layer’s shouldRasterize
to true
and its rasterizationScale
to some sensible value (probably UIScreen.main.scale
). You can always turn rasterization off again by setting shouldRasterize
to false
, so it’s easy to rasterize just before some massive or sluggish rearrangement of the screen and then unrasterize afterward.
In addition, there’s a layer property drawsAsynchronously
. The default is false
. If set to true
, the layer’s graphics context accumulates drawing commands and obeys them later on a background thread; the drawing commands themselves run very quickly, because they are not being obeyed at the time you issue them. I haven’t had occasion to use this, but presumably there could be situations where it keeps your app responsive when drawing would otherwise be time-consuming.
All of a layer’s properties are accessible through Cocoa key–value coding by way of keys with the same name as the property. These are two ways of doing the same thing:
layer.mask = mask // or: layer.setValue(mask, forKey: "mask")
In addition, CATransform3D and CGAffineTransform values can be expressed through key–value coding and key paths. Again, these are equivalent:
self.rotationLayer.transform = CATransform3DMakeRotation(.pi/4.0, 0, 1, 0) // or: self.rotationLayer.setValue(.pi/4.0, forKeyPath:"transform.rotation.y")
That notation is possible because CATransform3D is key–value coding compliant for a repertoire of keys and key paths. These are not properties; a CATransform3D doesn’t have a rotation
property. It doesn’t have any properties, because it isn’t even an object. You cannot say:
self.rotationLayer.transform.rotation.y = //... no, sorry
The transform key paths you’ll use most often are:
"rotation.x"
, "rotation.y"
, "rotation.z"
"rotation"
(same as "rotation.z"
)
"scale.x"
, "scale.y"
, "scale.z"
"translation.x"
, "translation.y"
, "translation.z"
"translation"
(two-dimensional, a CGSize)
The Quartz Core framework also injects key–value coding compliance into CGPoint, CGSize, and CGRect, allowing you to use keys and key paths matching their struct component names. For a complete list of KVC compliant classes related to CALayer, along with the keys and key paths they implement, see “Core Animation Extensions to Key-Value Coding” in Apple’s Core Animation Programming Guide in the documentation archive.
Moreover, you can treat a CALayer as a kind of dictionary, and get and set the value for any key. This means you can attach arbitrary information to an individual layer instance and retrieve it later. Earlier I mentioned that to apply manual layout to a layer’s sublayers, you will need a way of identifying those sublayers. This feature could provide a way of doing that:
myLayer1.setValue("manny", forKey:"pepboy") myLayer2.setValue("moe", forKey:"pepboy")
A layer doesn’t have a pepboy
property; the "pepboy"
key is something I’m attaching to these layers arbitrarily. Now I can identify them later by getting the value of their respective "pepboy"
keys.
Also, CALayer has a defaultValue(forKey:)
class method; to implement it, you’ll need to subclass and override. In the case of keys whose value you want to provide a default for, return that value; otherwise, return the value that comes from calling super
. In this way, a key can have a non-nil
value even if no value has ever been explicitly provided for that key.
The truth is that this feature, though delightful (and I often wish that all classes behaved like this), is not put there solely for your convenience and enjoyment. It’s there to serve as the basis for animation, which is the subject of the next chapter.