Understanding the iOS Layer Drawing & Display Hierarchy – A Detailed Look Behind the Scenes
12 Jun 2012

Understanding the iOS Layer Drawing & Display Hierarchy – A Detailed Look Behind the Scenes

12 Jun 2012

The first time I was confronted with the need to do some custom drawing in my iOS app, I spent longer understanding and deciding how to begin than I did actually doing the drawing code itself. Through much trial and error, and many pages of Apple docs and Stack Overflow posts, I journeyed through a steep, though ultimately satisfying, learning curve about how the CALayer drawing methods fit together. Hopefully this information will save you some time and give you a more concrete idea about how and where issues occur the next time you encounter that puzzling and frustrating blank screen.

Many Options for Drawing a CALayer

If you want to implement custom drawing code, iOS gives you no less than 2 methods in CALLayer, 2 in CALayerDelegate and a few other options for a mind boggling total of at least 6 design patterns to choose from before you even begin to draw. Here are your choices:

  1. Create the CALayer in the view’s awakeFromNib or init method and set the contents property directly
  2. Create and set the delegate on a CALayer and implement either one of
    1. -displayLayer:
    2. -drawLayer:inContext
  3. Subclass CALayer and…
    1. override an init method and set the contents property directly
    2. override -drawInContext:
    3. override -display (not recommended – use only if you know what you are doing)

If, like me, you are borderline OCD about code organisation, then you will likely have nearly maxed out your brain CPU trying to decide which of these options to choose in your project. At first they each seem to do about the same thing but each has it’s best use as we’ll see below. One quick note, most of these methods are mutually exclusive, so if you declare a -displayLayer: method, -drawLayer:inContext will NOT be called. More on this later…

Subclass vs. Delegate

Whether you choose to subclass or to use a delegate is really a matter of taste. Apple and the community at large tend to encourage delegates but the fact that the abstract methods in CALayer exist implies an approval of the subclass option as well. Personally, I generally choose a delegate pattern in the app’s main code for it’s tendency to remove bloat from the UIView or UIViewController subclass (it’s more SRP). Subclassing is useful for reusable framework-like components where you want the client code to be able to do something like:

Init, display or draw?

Performance is the main factor for the second decision axis. Drawing in Core Graphics takes a non-trivial amount of time. Take a look at the 2D versus the 3D views in the free Planets app. The 3D is way faster because it’s in OpenGL. The 2D is in Quartz and I’d hazard a guess that the app is redrawing the entire universe on each frame.

A better option, assuming your graphics don’t change as frequently as the screen’s refresh rate, is to pre-draw and cache them. Then you can just set the contents property of the layer on each redraw request:

Note, you still have the option to scale, translate, rotate, affine transform, adjust opacity, change blending options and perform several other modifications without needing to redraw the content.

Another reason to use this strategy would be if your layer will always be set to show a UIImage, regardless of whether it’s preloaded or not. In this case there is no need to draw anything with Quartz.

Sometimes you have a layer which you draw once and whose graphics then never change other than via moves or transforms. Perhaps you have a circle that tracks the user’s touch. In this case, it’s often wise to draw the layer in the init/awake phase in the UIView or perhaps in the CALayer’s subclass init override. Here’s an example…

This just leaves -drawLayer:inContext – which in some sense should be a last port of call. You might use it in any of the above scenarios solely for its convenience in handling the graphic context – just be sure to put measures in place to prevent unnecessary redrawing that would occur when -setNeedsDisplay is invoked, perhaps on a superview/superlayer. It might be useful in cases where you have a large image and you only wish to show a portion of it at any given time. The graphics context might represent the bounds that needs drawing.

I say last port of call only to discourage its being used to redraw all of the app’s graphics with any sort of frequency (as seems to be the case in the Planets app mentioned above). If your app has some sort of avant garde graphics which require atypical transforms for various user and time based parameters you’re very likely going to need OpenGL to get any sort of acceptable level of performance anyway.

Drawing Context and the Anatomy of CALayer::display

As you see in the examples above, you can create your own drawing context at any time and simply stash it away or assign its resulting image immediately to a layer. In effect, -drawInContext: and its delegate cousin -drawLayer:inContext are convenience methods which are invoked after all the graphics context hassle has been done for you and after which the image extraction and layer contents assignment is automatically handled. In other words, the code in -displayLayer: below, is what happens for you automatically if you don’t define it and instead rely only on drawLayer:inContext:

This magic and the whole subclass/delegate/display/draw hierarchy actually happens in CALayer’s -display method, which you invoke indirectly (never directly sayeth the docs) by calling -setNeedsDisplay:. Investigation also reveals that -display calls -drawInContext: when a delgate::displayLayer: is not available. -drawInContext: by default checks for it’s related method in the delegate. This is why overriding this method overrules the delegate’s -drawLayer:inContext:.

The code below aims to make all this clearer by mimicking the behaviour of the CALayer methods. Keep in mind that I can’t know for sure what other voodoo spells are being caste by the true Apple implementations so it’s really for educational purposes only.

From this we learn a few things:

  • The existence of methods higher up the chain such as delegate::displayLayer: prevent the processing of those lower down the chain such as layer::drawInContext:.
  • -display actual calls -drawInContext: unless you override it. This is actually the case with the internal CALayer code.
  • There are several avenues through which you can end up with a blank screen despite having working drawing code in a potential drawing method.

The Hierarchy Summary

In short the hierarchy of the CALayer and delegate drawing methods is as follows:

  1. CALayer::display
  2. CALayerDelagate::displayLayer:
  3. CALayer::drawInContext:
  4. CALayerDelegate::drawLayer:inContext

Finally, note that setting the contents property on a layer has the effect of undoing any previous calls to setNeedsDisplay.

UIView and drawRect – A Look into the Future of iOS?

One might deduce from a few aspects of the Apple iOS frameworks that there is a trend away from all the Core Graphics and CALayer business with all it’s ol’ school object “references” and C functions. UIBezierPath replaces many of the old Core Graphics functions and the UIGraphics*() functions serve as a bridge to allow easy context handling, often without needing to declare a CGContextRef, and to facilitate use of the new Objective-C style code alongside of the currently more complete but legacy Core-* style stuff.

Similarly, UIView’s drawRect: makes much of the above redundant. By the time it’s called, the CGContext has been created and pushed into the current context. Afterwards the image data is automatically extracted and assigned to the layer and any required clean up is taken care of.

drawRect: is actually invoked by a UIView implementation of the method drawLayer:inContext:. This works because by default a UIView is set as the delegate for its underlying layer. You can’t do this for sublayers, but in light of the aforementioned trend, I’d imagine iOS of the future would prefer for you to make subviews to sublayers anyway. This Stack Overflow post has an enlightening discussion on the matter.

Conclusion – It’s all the same until performance is an issue

The UIView drawRect: is definitely the simplest way to get your pen on the pad so to speak. UIView animations are also much more civilised than their Core Animation ancestors. However, as with many of the adolescent UIKit implementations, it doesn’t yet cover all the angles which the more mature Core Graphics paradigms do. For example, high performance sprites: If you used drawRect: you’d need to call UIImage::drawAtPoint: or something similar to draw in the (current) context which would then get re-rendered to an image and assigned to a layer – redundant and potentially slow.

The other main issue is performance. It would be a good to know once and for all how a complex hierarchy of nested views with touch disabled, compares to the a similar construct with a single view and nested layers. If they perform similarly then many of the seeming shortcomings of UIView’s can be overcome. For instance, the entire sprite image could be a UIImageView subview which is clipped by the parent view and shifted as per which sprite image you needed to see. Hopefully someone with some more experience in this area can chime in a let us know…

More Posts
Comments

Comments are closed.