Quartz 2D drawing



Relevant resources


Documentation


Web


Sample code - Apple


Sample code - other


Sample code - class


Quartz 2D


You can do a lot with the default user interface elements and their display properties, but sometimes you need to create custom elements, do some 2D drawing onscreen, or even generate PDFs within your application.  For this, you will need to use the Quartz 2D API.  Quartz 2D resides within the Core Graphics, so the two names are often synonymous.  In fact, many of the API functions we will discuss have the CG prefix for Core Graphics.


The Quartz API is procedural C (Carbon), rather than the Objective-C you're used to with higher-level Cocoa.  This can lead to code that is more difficult to read and understand, so I often find myself referring to "cheat sheets" of standard components.


One confusing item about Quartz drawing is the coordinate system.  On the iPhone, the origin, (0,0), is in the upper-left corner of the screen or view.  On the Mac, and in the standard Quartz coordinate space, the origin is the bottom-left corner of the screen or view.  This is further complicated by the fact that UIViews flip their underlying layers, inverting the standard Quartz coordinate space.


Graphics contexts


When you draw using Quartz functions, you need to supply a context to draw into.  A context is a canvas on which all of your drawing will be layered, in the order that you draw onto it.  There are three primary types of contexts: display contexts, PDF contexts, and bitmap contexts.


Display contexts


As we covered in the discussion about views and controllers, there is only one place within a UIView where you can draw to the screen: within the -drawRect: method.  You can only get a valid display context within this method.  To retrieve the current context, you use code like the following:


CGContextRef context = UIGraphicsGetCurrentContext();


The context is a CGContextRef, a pointer to a Core Foundation opaque type.  Core Foundation opaque types are similar to Objective-C classes in the way that they are managed, memory-wise.  Core Foundation objects created using functions that have "create" in their name must be released at some point in your code using CFRelease() or one of the more type-specific release functions.  In this case, the context is not created or retained, so we don't need to release it.


PDF contexts


In addition to drawing to the screen, you can use the same vector-based Quartz drawing commands to create PDF documents within your application.  Setting up a PDF context can be done in one of two ways: setting it to immediately write to a file, or configuring the context to write to data in memory.  I prefer the latter approach, because it lets you do things like attach NSData to an email message.


This is an example of creating a PDF contained within an NSData object:


NSMutableData *pdfData = [[NSMutableData alloc] init];

CGDataConsumerRef dataConsumer = CGDataConsumerCreateWithCFData((CFMutableDataRef)pdfData);

const CGRect mediaBox = CGRectMake(0.0f, 0.0f, drawingWidth, drawingHeight);

CGContextRef pdfContext = CGPDFContextCreate(dataConsumer, &mediaBox, NULL);

UIGraphicsPushContext(pdfContext);

CGContextBeginPage(pdfContext, &mediaBox);


// Draw your content here

CGContextEndPage(pdfContext);

CGPDFContextClose(pdfContext);

UIGraphicsPopContext();

CGContextRelease(pdfContext);

CGDataConsumerRelease(dataConsumer);


There are a few things going on here.  First, we create an NSMutableData instance and set it to be a data consumer (a destination for the PDF context to write to).  Because Core Graphics uses Core Foundation types, and not Cocoa classes, CGDataConsumerCreateWithCFData() requires a CFMutableDataRef argument.  We are able to simply cast the NSMutableData class we created as this type, because NSData is a toll-free bridged class.  What that means is that it can be used in either Cocoa methods or Core Foundation functions without conversion between types.


After that, we set the page size of the PDF context (in points) and create a PDF context, using the data consumer we set up before.  We then make this the active context for drawing by using UIGraphicsPushContext().


In this case, we are only creating a single page in the PDF we're drawing, so we begin the page, draw, then end the page.  If you wanted to do multiple pages, you could repeat this for each page.


Note that in order to avoid memory leaks, we release the context and data consumer when done with them.  We use type-specific release functions for these two objects we've created.


Another note is that all of this drawing will be done in the Quartz coordinate space, so if you've set up your drawing routines to display correctly in an iPhone view, it will be flipped here.  To counteract this flipping, you can place the following within your drawing code (after UIGraphicsPushContext()):


CGContextTranslateCTM(context, 0.0f, self.frame.size.height);

CGContextScaleCTM(context, 1.0f, -1.0f);


These are transforms, which I'll explain later.


In addition to saving to PDF contexts, you can also load PDFs for display.  This is done using the CGPDFDocument type.  Apple provides sample code for loading PDF documents in the Quartz 2D Programming Guide section "PDF Document Creation, Viewing, and Transforming".


Bitmap contexts


You can also draw directly to an image context, which is slightly simpler to set up than a PDF one:


UIGraphicsBeginImageContext(drawingSize);

CGContextRef context = UIGraphicsGetCurrentContext();

// Do your drawing here


UIImage *contextImage = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();


In this case, we create an image context of the same size as our image, draw in it, and then extract an autoreleased UIImage instance from the context.  We can then use this image in a number of different ways, including creating a JPEG out of it:


NSData *jpegData = UIImageJPEGRepresentation(contextImage, 0.9);


which returns an NSData instance containing the data for a JPEG impression at a 90% quality setting.


This does the same, only with PNG data:


NSData *pngData = UIImagePNGRepresentation(contextImage);


Colors and colorspaces


When dealing with colors in your Quartz drawing, it is possible to set the current colors for a context using the commands


CGContextSetRGBStrokeColor(context, redComponent, greenComponent, blueComponent, alphaComponent);

CGContextSetRGBFillColor(context, redComponent, greenComponent, blueComponent, alphaComponent);


where the various color components are normalized to 1.0 (that is, they are floating point values that go from 0.0 to 1.0).  Anecdotal evidence and limited testing indicates that setting colors in this fashion is slower than setting colors based on pre-existing color objects.


To create color objects, you first need to create a colorspace.  I tend to use a shared instance for this, using code like the following:


+ (CGColorSpaceRef)genericRGBSpace;

{ 

static CGColorSpaceRef space = NULL; 

if (space == NULL) 

{ 

space = CGColorSpaceCreateDeviceRGB();

} 

return space; 

} 


I also like caching commonly used colors for performance.  Again, shared instance code for this looks something like the following:


+ (CGColorRef)redColor; 

{ 

static CGColorRef red = NULL; 

if (red == NULL) 

{ 

CGFloat values[4] = {1.0, 0.0, 0.0, 1.0}; 

red = CGColorCreate([self genericRGBSpace], values); 

} 

return red; 

} 


With these kinds of class methods, you can use the faster CGContextSetStrokeColorWithColor() methods, and the like:


CGContextSetStrokeColorWithColor(context, [MyClass redColor]);

CGContextSetFillColorWithColor(context, [MyClass redColor]);


Graphic states


By changing the fill and stroke color of a context, you are changing its state.  Everything drawn after this point will have that state applied to it (in this case, all paths will have a red stroke and fill).  There are several other state properties you can change.  These include the shadow color, clipping path, and transform.


To store a graphics state to be retrieved later, you use the following:


CGContextSaveGState(context);


Then when you want to retrieve the previous graphics state, you use:


CGContextRestoreGState(context);


This lets you temporarily set graphics properties for a context, then be able to quickly return to the old values.


Shadow color


You can set it so that any paths drawn in your context have a shadow by using code like the following:


CGContextSetShadowWithColor( context, shadowOffset, shadowRadius, [shadowColor CGColor] );  


where shadowOffset is a CGSize struct that contains values for how much to offset the shadow right and up, and shadowRadius is the radius to extend the shadow out from the path.  The shadow can easily be made a glow by using a non-greyscale color and by using a (o,o) offset on the shadow.


Again, everything drawn after this in the context will have the shadow or glow drawn for it.


Clipping path


There are times when you might want to clip your drawing to within a particular region.  This is most common when dealing with gradients.  To do this, first create a path (see below for that) and then call


CGContextClip(context);


This will clip all drawing to within the path you have defined.  Again, this is useful for drawing gradients like you'd see in glossy highlights.


Masking


Similar to clipping, you can mask part of your drawing based on an image's alpha channel.  You use code like to following for this:


CGContextClipToMask(context, maskRect, alphaImage);


Transform


Transforms let you move, scale, rotate, and otherwise manipulate your drawing.  The context has a current transformation matrix (CTM), which contains within it all of the manipulations you've made to the context's geometry.  You can directly manipulate the current transformation matrix using code like the following:


CGContextTranslateCTM (context, shiftRight, shiftUp);

CGContextScaleCTM(context, 0.5f, 0.5f);

CGContextRotateCTM(context, M_PI / 4.0f);


These commands shift the drawing after this point in the context, scale it, and rotate it, respectively.  Note that the order in which you apply these commands matters, and altering that order can produce vastly different drawings.


You can also create 2-D affine transforms, then apply the transforms all at once.  We were introduced to these transforms when we covered views.  You can create a new affine transform based on an operation using CGAffineTransformMakeTranslation(), CGAffineTransformMakeRotation(), etc.  These transforms can then be further manipulated using CGAffineTransformTranslate(), CGAffineTransformRotate(), etc.  An example of this is as follows:


CGAffineTransform affineTransform = CGAffineTransformMakeTranslation(10.0f, 20.0f);

affineTransform = CGAffineTransformRotate(affineTransform, M_PI / 4.0f);


This transform now contains the matrix required to translate drawing by (10.0, 20.0), then rotate it by 45 degrees.  To apply this transform to the context, you would use


CGContextConcatCTM(context, affineTransform);


As mentioned before, these transforms operate in the same way when you set them to the transform property of a UIView.


You can also apply a transform to a CGPoint, CGSize, or CGRect struct using CGPointApplyAffineTransform(), CGSizeApplyAffineTransform(), and CGRectApplyAffineTransform(), respectively.


Drawing paths


Paths are vector elements drawn within a Quartz context.  They can be simple or arbitrarily complex, with a stroke and / or fill.


Another peculiarity about Quartz drawing comes in how paths are pixel-aligned.  When dealing with views and layers, you align the origin of the view to integral (non-fractional) values in X and Y to ensure sharp rendering.  Quartz instead aligns elements midway between pixels.  This means that you need to draw on half-pixel coordinates (1.5, 2.5) in order to end up with sharply drawn paths, etc.


You can start a path with


CGContextBeginPath(context);


and end it with 


CGContextClosePath(context);


To draw that path you use


CGContextDrawPath(context, kCGPathFillStroke);


where the last argument is the drawing mode: kCGPathFill, kCGPathEOFill, kCGPathStroke, kCGPathFillStroke, or kCGPathEOFillStroke. These control whether just the stroke, just the fill, both, or an alternate fill mode is used to draw the path.


You can also create paths for use outside of drawing using a CGMutablePathRef type.  The following is an example of this:


CGMutablePathRef curvedPath = CGPathCreateMutable();

CGPathMoveToPoint(curvedPath, NULL, xCoordinate1, yCoordinate1);

CGPathAddCurveToPoint(curvedPath, NULL, xCoordinate2, yCoordinate2, xCoordinate2, yCoordinate1, xCoordinate2, yCoordinate2);


// Use the path for something


CGPathRelease(curvedPath);


We will see a use for this when we cover Core Animation CAKeyframeAnimations.


Lines


Lines are drawn by first moving the path to the start point of the line, then adding a line segment to a point:


CGContextBeginPath(context);

CGContextMoveToPoint(context, 10.5f, 10.5f);

CGContextAddLineToPoint(context, 20.5f, 20.5f);

CGContextClosePath(context);

CGContextDrawPath(context, kCGPathFillStroke);


The preceding code would draw just a line from (10.5, 10.5) to (20.5, 20.5).


For lines, there are a few additional context state elements of interest:


CGContextSetLineCap(context, kCGLineCapRound);

CGContextSetLineJoin(context, kCGLineJoinRound);

CGContextSetLineWidth(context, strokeWidth);


These set the end cap type of the lines, the join style of line segments, and the line width.  Again, set these before you do your line drawing to have then take effect.


Bezier curves, quadratic curves, and arcs


To draw curved elements, you have a few options.  You can draw Bezier or quadratic curves, which use control points to determine their curvature, or arcs, which are simply segments cut from a circle.


To add a Bezier segment to a path, you'd use code like the following:


CGContextAddCurveToPoint(context, controlPoint1X, controlPoint1Y, controlPoint2X, controlPoint2Y, endCoordinateX, endCoordinateY);


The control point coordinates specify two points for the start and end of the curve to determine its curvature.


Likewise, a quadratic curve can be added to a point using the following:


CGContextAddQuadCurveToPoint(context, controlPointX, controlPointY, endCoordinateX, endCoordinateY);


Finally, you can draw an arc using 


CGContextAddArc(context, centerX, centerY, radius, startAngle, endAngle, clockwise);


or


CGContextAddArcToPoint(context, centerX, centerY, endPointX, endPointY, radius);


Text drawing


It's pretty easy to do custom text drawing using the UIKit additions to the NSString class.  For example,


[text drawAtPoint:CGPointMake(0.5f, 0.5f) withFont:[UIFont fontWithName:@"Helvetica" size:16.0f]];


will draw the text string at (0.5, 0.5) in 16-point Helvetica.  However, this text drawing can be pretty slow because it calls back to the web rendering engine to format the text.


For Quartz-only text drawing, you can use code like the following:


CGContextSelectFont(context, "Helvetica", 16.0f, kCGEncodingMacRoman);

CGContextSetTextDrawingMode(context, kCGTextFill);

CGContextSetTextPosition(context, 0.0f, round(16.0f / 4.0f));

if ([text canBeConvertedToEncoding:NSMacOSRomanStringEncoding])

{

CGContextShowText(context, [text cStringUsingEncoding:NSMacOSRomanStringEncoding], strlen([text cStringUsingEncoding:NSMacOSRomanStringEncoding]));

}


While fast, this requires that you convert your NSString to the MacRoman character set, which does not contain many international symbols.  It is for this reason that I only use the slower UIKit text drawing additions in my code.


Gradients


You can do both linear and radial gradients in Quartz.


The following is an example of a linear gradient for drawing a glossy highlight over a view:


CGGradientRef glossGradient;

CGColorSpaceRef rgbColorspace;

size_t num_locations = 2;

CGFloat locations[2] = { 0.0, 1.0 };

CGFloat components[8] = { 1.0, 1.0, 1.0, 0.35,  // Start color

      1.0, 1.0, 1.0, 0.06 }; // End color

rgbColorspace = CGColorSpaceCreateDeviceRGB();

glossGradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);

CGRect currentBounds = self.bounds;

CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);

CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMidY(currentBounds));

CGContextDrawLinearGradient(currentContext, glossGradient, topCenter, midCenter, 0);

CGGradientRelease(glossGradient);

CGColorSpaceRelease(rgbColorspace); 


To construct a linear gradient, you must specify an arbitrary number of colors as their RGBA components to use as stops in the gradient.  These stops must be accompanied by an array of positions within the gradient that they occur at.


You then create the gradient in a given colorspace, define the coordinates that correspond with the start and end of the gradient, and draw it in the context.  Normally, this will draw everywhere between the specified points, but not before or after.  You can limit the area to draw the gradient over using a clipping path, and by passing kCGGradientDrawsAfterEndLocation the gradient will continue past its last point indefinitely.


The following is an example of a radial gradient:


CGGradientRef backgroundGradient;

CGColorSpaceRef rgbColorspace;

size_t num_locations = 2;

CGFloat locations[2] = { 0.0, 1.0 };

CGFloat components[8] = { 0.0, 0.0712, 0.4, 1.0,  // Start color

      0.0, 0.0, 0.1294, 1.0 }; // End color

rgbColorspace = CGColorSpaceCreateDeviceRGB();

backgroundGradient = CGGradientCreateWithColorComponents (rgbColorspace, components, locations, num_locations);

CGPoint centerPoint = CGPointMake(240.0f, 150.0f);

CGContextDrawRadialGradient (UIGraphicsGetCurrentContext(), backgroundGradient, centerPoint, 0.0f, centerPoint, 240.0f, kCGGradientDrawsAfterEndLocation);

CGGradientRelease(backgroundGradient);

CGColorSpaceRelease(rgbColorspace);