For anyone who’s developed exclusively with UIViews on iOS may take the title of this post a bit oddly. “WHAT?!” they might say, “Are you insane? Core Graphics is not only a C-only API, but has confusing function names, and needs way more code to do the same thing I can do in less code in UIView”. Yes, they might be right, but there’s a reason why Core Graphics exists. It’s FAST!
But using Core Graphics doesn’t mean that your code has to be confusing, or that you have to compromise flexibility for performance. You can have your cake and eat it too (aka you can have high-performing code that is easy to read). Read on to see what I mean.
Baby steps in using drawRect:
The drawRect: method is your entry point to drawing in Core Graphics, and is (for the most part) the only place where drawing operations can be performed. This is the biggest problem newbies have when getting started with low-level drawing on iOS because they’re used to being able to add subViews or subLayers arbitrarily in whatever method they happen to be in.
drawRect:
is called by the underlying graphics subsystem in its main frame rendering loop. When it’s time to draw the next frame, iOS digs through all the views on the screen and checks to see if its content needs to be updated. If it does, then your drawRect:
method is called with the appropriate rect that needs to be updated. It’s as simple as that. You get asked to draw your content when, and only when, it needs something from you.
Finding your context
All drawing operations in Core Graphics utilize a context pointer to keep track of what region your drawing operations will be made to. This is a standard thing you’ll find in C APIs because without an Object-Oriented “self” property easily available, you have to provide some means of providing context.
- (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); // Do something with your context }
From that point on you can use the “ctx
” variable for all subsequent Core Graphics calls.
Saving state
If you want to draw anything on the screen, even if it’s something as simple as a single line, you’ll need to use several function calls to do so. This is because separate calls are used to set attributes such as the line colour, the line width, any end-cap settings you may want for the corners of your line, and then the actual path that your line will follow. Core Graphics keeps track of all these different attributes by updating its internal state.
Think of the drawing state as a pen: you change the tip, you change the colour, and then you can draw many separate lines with the same pen. But if you forget to change your pen back, you can mess future drawing calls up if you don’t clean up after yourself.
To work around problems like this it’s best to use the CGContextSaveGState
and CGContextRestoreGState
functions. Think of it as saving a bookmark or a checkpoint of your current graphics state. You can then change any drawing attributes you like, you can draw or fill or stroke any path, and when you’re done you reset the graphics state back to what it was before.
There is one important catch however: the number of times you call Save and Restore must match perfectly, because otherwise you may screw up the drawing state for whomever is calling your class. This is important because otherwise some really weird garbage may end up getting drawn to your screen. To get around this, I use a very simple technique that I’ll show in the code sample below.
- (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSaveGState(ctx); { // Draw whatever you like } CGContextRestoreGState(ctx); }
By including a new set of brackets in your code, you give yourself a visual indentation of code that is covered by a set of save and restore calls.
Simple drawing using UIView classes
There are some fairly simple operations that some people want to do that are made easy by a few simple helper methods provided in a few UIView subclasses. For example, assume you’re building the next greatest Twitter application and you want to squeeze as much performance out of your feed items as you possibly can. Obviously rendering text, images, gradients, borders and shadows are fairly expensive operations if you want to handle scrolling through thousands of feed posts at 60fps.
Fortunately you don’t have to replace your UIView classes with hundreds of lines of Core Graphics calls. Many of the most common classes like UIImage, UIBezierPath and so on, all provide convenience methods for drawing their content within drawRect: using Core Graphics directly.
Update: The original post had a few bugs in it, mostly due to writing this post exclusively from memory during the Christmas holidays. As a result of some very helpful comments on this post however, I’ve had to make some changes. I apologize for the bugs in the first version.
UIImage drawing
We can use UIImage objects to directly draw to a Core Graphics context. Using raw API calls makes for very verbose code, since you’d have to create a CGImage object, a color-space object, and so forth. Instead, drawing an image can be as easy as the following.
- (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); UIImage *img = [UIImage imageNamed:@"MyImage.png"]; [img drawInRect:rect]; }
With this technique you have many more options, such as blending and alpha arguments. There are other methods that let you draw an image at a particular point, drawing a pattern, and so forth.
UIBezierPath drawing
Often times you might want to show rounded corners on some view, but you only want certain corners to be rounded. For example lets assume you want to show a container view with only the top two corners rounded. The most straight-forward strategy is to drop into Core Graphics to render. While it’s possible to compose a path using low-level APIs alone, it’s quite simple to do the following.
- (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor); CGContextSetLineWidth(ctx, 3); UIBezierPath *path; path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:(UIRectCornerTopLeft | UIRectCornerTopRight) cornerRadii:10.0]; [path stroke]; }
Responding to user input
Core Graphics tries to be as efficient as possible, and so it takes some shortcuts on your behalf. For the most part these are exactly what you want and would expect, and come as a pleasant surprise when you come from other platforms. But there are times that it can get in your way, so knowing in advance what those optimizations are and how to control them helps. And knowing is half the battle…
Knowing when your view “Needs Display”
Your drawRect:
method will only be invoked when UIKit feels your content has become stale and needs to be redrawn. For the most part this typically means your drawRect:
method will be called when the view is added to a superview. Since drawRect is called when the frame is drawn, calling it explicitly won’t work as you’d expect, so if some user interaction requires you to redraw your content, there’s a simple solution for that.
All UIView objects have a method called “setNeedsDisplay
“. Invoking this will toggle a flag inside your object indicating that your view needs to be redrawn upon the next frame. This has several advantages, including the fact that it makes your code very quick; this is because no matter how many times you call it within the same runloop, your frame will still only be redrawn once on the next frame. Therefore you don’t have to worry about over-invoking it, ensuring that you can keep your content fresh from wherever your UI logic determine content needs to change.
- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { CGRect labelRect = CGRectMake(32, CGRectGetHeight(frame) - 32, CGRectGetWidth(frame), 32); self.likeLabel = [[[UILabel alloc] initWithFrame:labelRect] autorelease]; self.likeLabel.font = [UIFont systemFontWithSize:12.0]; self.likeLabel.autoresizingMask = (UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin); [self addSubview:self.likeLabel]; self.likeIcon = [UIImage imageNamed:@"likeIcon.png"]; self.unlikedIcon = [UIImage imageNamed:@"unlikedIcon.png"]; } } - (void)dealloc { self.likeLabel = nil; self.likeIcon = nil; self.unlikedIcon = nil; [super dealloc]; } - (IBAction)likeClicked:(id)sender { // Update the label's text self.isLiked = !self.isLiked; self.likeLabel.text = (self.isLiked) ? @"Liked by you" : @""; [self setNeedsDisplay]; } - (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); // Determine the coordinates we want our content drawn at CGPoint iconPoint = CGPointMake(floorf(self.likeIcon.size.width / 2), CGRectGetMidY(self.textLabel.frame)); // Draw the content UIImage *image = (self.isLiked) ? self.likedIcon : self.unlikedIcon; [image drawAtPoint:iconPoint]; }
In the code example above you’ll notice that we’re creating the UIImage object in initWithFrame:
, and are destroying it in dealloc, so that we don’t have to allocate objects within the drawRect: method. It’s important to keep that method as quick as possible so you don’t negatively impact your screen’s framerate.
Responding to frame size changes
When your view changes its size, either when explicitly set or when a superview autoresizes its subviews, your content will be invalidated in some way. But rather than calling expensive drawRect: methods all the time when this happens, UIView objects have a contentMode
property that lets you give UIKit a hint about what you want it to do in this situation.
This property is an typedef enum
named UIViewContentMode
and has a bunch of different options that you should check out. The default value is UIViewContentModeScaleToFill
which means if your frame’s size changes, UIKit will simply take the last-rendered result your drawRect:
method created and will scale it up or down to fill the available space.
The simplest way to get your content to redraw is to set your view’s contentMode
to UIViewContentModeRedraw
, but in many cases this may be overkill.
For example, lets assume your drawRect:
method draws a border and some comments along the bottom of your frame, as the “Like” example does in the previous section. If your frame changes its height it would be wasteful to perform a full redrawing operation when most of the content hasn’t changed. So if you set the contentMode
to UIViewContentModeBottomLeft
UIKit will align the content in the bottom-left corner of the view, cropping the rest of the content as it does so.
Avoid drawing across pixel boundaries
When you position elements on the screen (not just with Core Graphics, but in UIKit in general) you need to make sure that your views exist at integer pixel coordinates. iOS’ drawing coordinate system works in points, not absolute pixels, which means it’s possible to tell UIKit or Core Graphics to draw content at a fractional-pixel. Since this is impossible, the device attempts to anti-alias your content so that it blurs between several pixels. This is almost never what you want, so when calculating positions (especially when dividing one value by another) it’s important to use the floorf
or ceilf
functions to round the values down or up to the nearest integer.
This is more obvious when drawing or stroking lines or paths on the screen. Lets take the following example where we draw a single line along the bottom of the view. For this example however we’ll use the low-level drawing routines to compose a path, instead of using a UIBezierPath
object.
- (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor); CGContextSetLineWidth(ctx, 1); CGContextBeginPath(ctx); CGContextMoveToPoint(ctx, 0, CGRectGetMaxY(rect)); CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect)); CGContextStrokePath(ctx); }
This code seems pretty straightforward; move the drawing “pen” position to the bottom-left corner, add a line to the bottom-right corner, and stroke the resulting path with the line width and line color previously defined.
The “gotcha” here is that when you create a point at (0, 0)
, that represents the logical upper-left corner of the screen. However the pixel in the top-left corner occupies the space defined by the frame (0, 0, 1, 1)
(e.g. a 1-point square starting at (0, 0)). When you draw a line it is centred on whatever coordinates you give it. Therefore in the code example above, your code is asking Core Graphics to draw a line that has 0.5 in one pixel, and 0.5 in another pixel.
To get around this you should offset your drawing frame so that you draw 0.5 points offset. Here’s an updated version of that code which draws a solid unblurred line:
- (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor); CGContextSetLineWidth(ctx, 1); CGContextBeginPath(ctx); CGContextMoveToPoint(ctx, 0, CGRectGetMaxY(rect)) - 0.5; CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect)); CGContextStrokePath(ctx); }
Drawing with colour the easy way
Core Graphics has evolved over time, with new and improved APIs. This means that for many drawing operations there might be several different function calls that will give you the same result. This is especially true when dealing with colour.
For example, if you want to set the stroke (aka “line”) colour in Core Graphics, there are four separate function calls that will achieve the same result. Your impulse might be to pick the first result that comes up, but that means more work for yourself. The standard
CGContextSetStrokeColor
function call takes an array of CGFloat
s indicating the different colour values you want to draw with.
However there’s a much easier way. CGContextSetStrokeColorWithColor
is a slightly more verbose function name, but allows you to supply a CGColorRef
value to describe a colour. And as a convenience, the UIColor
class provides a helper property called CGColor
that returns a pre-calculated colour reference, which automatically releases its memory when the UIColor
object goes away. This means you don’t need to manually malloc or release your colour references, and results in much less code.
You can see some of the code examples up above where I’ve already used this function and property. Whenever you see a function that has a “ColorWithColor” option, it uses this pattern.
Use the right tool for the job
Perhaps the best advice I can give is to use the right tool for the job. For example, if you need to visually style some interface element, you shouldn’t add unnecessary subviews just to draw things like borders, rounded corners, tiled patterns, and so on. Additionally there are cases where having a large number of subviews (such as in UITableViewCells) is inefficient and rendering simple elements like icons, text, or borders is better suited to Core Graphics.
iOS and UIKit is really a different environment than HTML, and you can’t treat device layout and rendering the same way.
And finally, make sure you practice, and don’t be afraid of trying out Core Graphics. Start simple with things like borders, rendering images and text, or programmatically rendering effects that would otherwise be slow using Core Animation or image-based textures.