iOS Reference Library Apple Developer
Search

Graphics and Drawing

In addition to the standard frameworks you use for drawing, iOS 3.2 introduces some new features for generating rendered content. The UIBezierPath class is an Objective-C wrapper around a Core Graphics path that makes creating vector-based paths easier. And if you use PDF content, there are now functions that you can use to generate PDF data and save it to a file or data object.

Drawing Shapes Using Bezier Paths

In iOS 3.2, you can now use the UIBezierPath class to create vector-based paths. This class is an Objective-C wrapper for the path-related features in the Core Graphics framework. You can use the class to define simple shapes, such as ovals and rectangles, as well as complex complex shapes that incorporate multiple straight and curved line segments.

You use path objects to draw shapes in your application’s user interface. You can draw the path’s outline, fill the space it encloses, or both. You can also use paths to define a clipping region for the current graphics context, which you can then use to modify subsequent drawing operations in that context.

Bezier Path Basics

A UIBezierPath object is a wrapper for a CGPathRef data type. Paths are vector-based shapes that are built using line and curve segments. You use line segments to create rectangles and polygons and you use curve segments to create arcs, circles, and complex curved shapes. Each segment consists of one or more points (in the current coordinate system) and a drawing command that defines how those points are to be interpreted. The end of one line or curve segment defines the beginning of the next. Each set of connected line and curve segments form what is referred to as a subpath. And a single UIBezierPath object may contain one or more subpaths that define the overall path.

The processes for building and using a path object are separate. Building the path is the first process and involves the following steps:

  1. Create the path object.

  2. Set the starting point of the initial segment using the moveToPoint: method.

  3. Add line and curve segments to define one or more subpaths.

  4. Modify any relevant drawing attributes of your UIBezierPath object. For example, you might set the lineWidth or lineJoinStyle properties for stroked paths or the usesEvenOddFillRule property for filled paths. You can always change these values later as needed.

When building your path, you should arrange the points of your path relative to the origin point (0, 0). Doing so makes it easier to move the path around later. During drawing, the points of your path are applied as-is to the coordinate system of the current graphics context. If your path is oriented relative to the origin, all you have to do to reposition it is apply an affine transform with a translation factor to the current graphics context. The advantage of modifying the graphics context (as opposed to the path object itself) is that you can easily undo the transformation by saving and restoring the graphics state.

To draw your path object, you use the stroke and fill methods. These methods render the line and curve segments of your path in the current graphics context. The rendering process involves rasterizing the line and curve segments using the attributes of the path object. The rasterization process does not modify the path object itself. As a result, you can render the same path object multiple times in the current context.

Adding Lines and Polygons to Your Path

Lines and polygons are simple shapes that you build point-by-point using the moveToPoint: and addLineToPoint: methods. The moveToPoint: method sets the starting point of the shape you want to create. From that point, you create the lines of the shape using the addLineToPoint: method. You create the lines in succession, with each line being formed between the previous point and the new point you specify.

Listing 6-1 shows the code needed to create a pentagon shape using individual line segments. This code sets the initial point of the shape and then adds four connected line segments. The fifth segment is added by the call to the closePath method, which connects the last point (0, 40) with the first point (100, 0).

Listing 6-1  Creating a pentagon shape

UIBezierPath*    aPath = [UIBezierPath bezierPath];
 
// Set the starting point of the shape.
[aPath moveToPoint:CGPointMake(100.0, 0.0)];
 
// Draw the lines
[aPath addLineToPoint:CGPointMake(200.0, 40.0)];
[aPath addLineToPoint:CGPointMake(160, 140)];
[aPath addLineToPoint:CGPointMake(40.0, 140)];
[aPath addLineToPoint:CGPointMake(0.0, 40.0)];
[aPath closePath];

Using the closePath method not only ends the shape, it also draws a line segment between the first and last points. This is a convenient way to finish a polygon without having to draw the final line.

Adding Arcs to Your Path

The UIBezierPath class provides support for initializing a new path object with an arc segment. The parameters of the bezierPathWithArcCenter:radius:startAngle:endAngle:clockwise: method define the circle that contains the desired arc and the start and end points of the arc itself. Figure 6-1 shows the components that go into creating an arc, including the circle that defines the arc and the angle measurements used to specify it. In this case, the arc is created in the clockwise direction. (Drawing the arc in the counterclockwise direction would paint the dashed portion of the circle instead.) The code for creating this arc is shown in Listing 6-2.

Figure 6-1  An arc in the default coordinate system

Listing 6-2  Creating a new arc path

// pi is approximately equal to 3.14159265359
#define   DEGREES_TO_RADIANS(degrees)  ((pi * degrees)/ 180)
 
- (UIBezierPath*)createArcPath
{
   UIBezierPath* aPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(150, 150)
                           radius:75
                           startAngle:0
                           endAngle:DEGREES_TO_RADIANS(135)
                           clockwise:YES];
   return aPath;
}

If you want to incorporate an arc segment into the middle of a path, you must modify the path object’s CGPathRef data type directly. For more information about modifying the path using Core Graphics functions, see “Modifying the Path Using Core Graphics Functions.”

Adding Curves to Your Path

The UIBezierPath class provides support for adding cubic and quadratic Bézier curves to a path. Curve segments start at the current point and end at the point you specify. The shape of the curve is defined using tangent lines between the start and end points and one or more control points. Figure 6-2 shows approximations of both types of curve and the relationship between the control points and the shape of the curve. The exact curvature of each segment involves a complex mathematical relationship between all of the points and is well documented online and at Wikipedia.

Figure 6-2  Curve segments in a path

To add curves to a path, you use the following methods:

Because curves rely on the current point of the path, you must set the current point before calling either of the preceding methods. Upon completion of the curve, the current point is updated to the new end point you specified.

Creating Oval and Rectangular Paths

Ovals and rectangles are common types of paths that are built using a combination of curve and line segments. The UIBezierPath class includes the bezierPathWithRect: and bezierPathWithOvalInRect: convenience methods for creating paths with oval or rectangular shapes. Both of these methods create a new path object and initialize it with the specified shape. You can use the returned path object right away or add more shapes to it as needed.

If you want to add a rectangle to an existing path object, you must do so using the moveToPoint:, addLineToPoint:, and closePath methods as you would for any other polygon. Using the closePath method for the final side of the rectangle is a convenient way to add the final line of the path and also mark the end of the rectangle subpath.

If you want to add an oval to an existing path, the simplest way to do so is to use Core Graphics. Although you can use the addQuadCurveToPoint:controlPoint: to approximate an oval surface, the CGPathAddEllipseInRect function is much simpler to use and more accurate. For more information, see “Modifying the Path Using Core Graphics Functions.”

Modifying the Path Using Core Graphics Functions

The UIBezierPath class is really just a wrapper for a CGPathRef data type and the drawing attributes associated with that path. Although you normally add line and curve segments using the methods of the UIBezierPath class, the class also exposes a CGPath property that you can use to modify the underlying path data type directly. You can use this property when you would prefer to build your path using the functions of the Core Graphics framework.

There are two ways to modify the path associated with a UIBezierPath object. You can modify the path entirely using Core Graphics functions, or you can use a mixture of Core Graphics functions and UIBezierPath methods. Modifying the path entirely using Core Graphics calls is easier in some ways. You create a mutable CGPathRef data type and call whatever functions you need to modify its path information. When you are done you assign your path object to the corresponding UIBezierPath object, as shown in Listing 6-3.

Listing 6-3  Assigning a new CGPathRef to a UIBezierPath object

// Create the path data
CGMutablePathRef cgPath = CGPathCreateMutable();
CGPathAddEllipseInRect(cgPath, NULL, CGRectMake(0, 0, 300, 300));
CGPathAddEllipseInRect(cgPath, NULL, CGRectMake(50, 50, 200, 200));
 
// Now create the UIBezierPath object
UIBezierPath* aPath = [UIBezierPath bezierPath];
aPath.CGPath = cgPath;
aPath.usesEvenOddFillRule = YES;
 
// After assigning it to the UIBezierPath object, you can release
// your CGPathRef data type safely.
CGPathRelease(cgPath);

If you choose to use a mixture of Core Graphics functions and UIBezierPath methods, you must carefully move the path information back and forth between the two. Because a UIBezierPath object owns its underlying CGPathRef data type, you cannot simply retrieve that type and modify it directly. Instead, you must make a mutable copy, modify the copy, and then assign the copy back to the CGPath property as shown in Listing 6-4.

Listing 6-4  Mixing Core Graphics and UIBezierPath calls

UIBezierPath*    aPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 300, 300)];
 
// Get the CGPathRef and create a mutable version.
CGPathRef cgPath = aPath.CGPath;
CGMutablePathRef  mutablePath = CGPathCreateMutableCopy(cgPath);
 
// Modify the path and assign it back to the UIBezierPath object
CGPathAddEllipseInRect(mutablePath, NULL, CGRectMake(50, 50, 200, 200));
aPath.CGPath = mutablePath;
 
// Release both the mutable and immutable copies of the path.
CGPathRelease(cgPath);
CGPathRelease(mutablePath);

Rendering the Contents of a Bezier Path Object

After creating a UIBezierPath object, you can render it in the current graphics context using its stroke and fill methods. Before you call these methods, though, there are usually a few other tasks to perform to ensure your path is drawn correctly:

Listing 6-5 shows a sample implementation of a drawRect: method that draws an oval in a custom view. The upper-left corner of the oval’s bounding rectangle is located at the point (50, 50) in the view’s coordinate system. Because fill operations paint right up to the path boundary, this method fills the path before stroking it. This prevents the fill color from obscuring half of the stroked line.

Listing 6-5  Drawing a path in a view

- (void)drawRect:(CGRect)rect
{
    // Create an oval shape to draw.
    UIBezierPath* aPath = [UIBezierPath bezierPathWithOvalInRect:
                                CGRectMake(0, 0, 200, 100)];
 
    // Set the render colors
    [[UIColor blackColor] setStroke];
    [[UIColor redColor] setFill];
 
    CGContextRef aRef = UIGraphicsGetCurrentContext();
 
    // If you have content to draw after the shape,
    // save the current state before changing the transform
    //CGContextSaveGState(aRef);
 
    // Adjust the view's origin temporarily. The oval is
    // now drawn relative to the new origin point.
    CGContextTranslateCTM(aRef, 50, 50);
 
    // Adjust the drawing options as needed.
    aPath.lineWidth = 5;
 
    // Fill the path before stroking it so that the fill
    // color does not obscure the stroked line.
    [aPath fill];
    [aPath stroke];
 
    // Restore the graphics state before drawing any other content.
    //CGContextRestoreGState(aRef);
}

Doing Hit-Detection on a Path

To determine whether a touch event occurred on the filled portion of a path, you can use the containsPoint: method of UIBezierPath. This method tests the specified point against all closed subpaths in the path object and returns YES if it lies on or inside any of those subpaths.

Important: The containsPoint: method and the Core Graphics hit-testing functions operate only on closed paths. These methods always return NO for hits on open subpaths. If you want to do hit detection on an open subpath, you must create a copy of your path object and close the open subpaths before testing points.

If you want to do hit-testing on the stroked portion of the path (instead of the fill area), you must use Core Graphics. The CGContextPathContainsPoint function lets you test points on either the fill or stroke portion of the path currently assigned to the graphics context. Listing 6-6 shows a method that tests to see whether the specified point intersects the specified path. The inFill parameter lets the caller specify whether the point should be tested against the filled or stroked portion of the path. The path passed in by the caller must contain one or more closed subpaths for the hit detection to succeed.

Listing 6-6  Testing points against a path object

- (BOOL)containsPoint:(CGPoint)point onPath:(UIBezierPath*)path inFillArea:(BOOL)inFill
{
   CGContextRef context = UIGraphicsGetCurrentContext();
   CGPathRef cgPath = path.CGPath;
   BOOL    isHit = NO;
 
   // Determine the drawing mode to use. Default to
   // detecting hits on the stroked portion of the path.
   CGPathDrawingMode mode = kCGPathStroke;
   if (inFill)
   {
      // Look for hits in the fill area of the path instead.
      if (path.usesEvenOddFillRule)
         mode = kCGPathEOFill;
      else
         mode = kCGPathFill;
   }
 
   // Save the graphics state so that the path can be
   // removed later.
   CGContextSaveGState(context);
   CGContextAddPath(context, cgPath);
 
   // Do the hit detection.
   isHit = CGContextPathContainsPoint(context, point, mode);
 
   CGContextRestoreGState(context);
 
   return isHit;
}

Generating PDF Content

In iOS 3.2, the UIKit framework provides a set of functions for generating PDF content using native drawing code. These functions let you create a graphics context that targets a PDF file or PDF data object. You can then draw into this graphics context using the same UIKit and Core Graphics drawing routines you use when drawing to the screen. You can create any number of pages for the PDF, and when you are done, what you are left with is a PDF version of what you drew.

Figure 6-3 shows the workflow for creating a PDF file on the local filesystem. The UIGraphicsBeginPDFContextToFile function creates the PDF context and associates it with a filename. After creating the context, you open the first page using the UIGraphicsBeginPDFPage function. Once you have a page, you can begin drawing your content for it. To create new pages, simply call UIGraphicsBeginPDFPage again and begin drawing. When you are done, calling the UIGraphicsEndPDFContext function closes the graphics context and writes the resulting data to the PDF file.

Figure 6-3  Workflow for creating a PDF document

The following sections describe the PDF creation process in more detail using a simple example. For information about the functions you use to create PDF content, see UIKit Function Reference.

Creating and Configuring the PDF Context

You create a PDF graphics context using either the UIGraphicsBeginPDFContextToData or UIGraphicsBeginPDFContextToFile function. These functions create the graphics context and associate it with a destination for the PDF data. For the UIGraphicsBeginPDFContextToData function, the destination is an NSMutableData object that you provide. And for the UIGraphicsBeginPDFContextToFile function, the destination is a file in your application’s home directory.

PDF documents organize their content using a page-based structure. This structure imposes two restrictions on any drawing you do:

The functions you use to create a PDF graphics context allow you to specify a default page size but they do not automatically open a page. After creating your context, you must explicitly open a new page using either the UIGraphicsBeginPDFPage or UIGraphicsBeginPDFPageWithInfo function. And each time you want to create a new page, you must call one of these functions again to mark the start of the new page. The UIGraphicsBeginPDFPage function creates a page using the default size, while the UIGraphicsBeginPDFPageWithInfo function lets you customize the page size and other page attributes.

When you are done drawing, you close the PDF graphics context by calling the UIGraphicsEndPDFContext. This function closes the last page and writes the PDF content to the file or data object you specified at creation time. This function also removes the PDF context from the graphics context stack.

Listing 6-7 shows the processing loop used by an application to create a PDF file from the text in a text view. Aside from three function calls to configure and manage the PDF context, most of the code is related to drawing the desired content. The textView member variable points to the UITextView object containing the desired text. The application uses the Core Text framework (and more specifically a CTFramesetterRef data type) to handle the text layout and management on successive pages. The implementations for the custom renderPageWithTextRange:andFramesetter: and drawPageNumber: methods are shown in Listing 6-8.

Listing 6-7  Creating a new PDF file

- (IBAction)savePDFFile:(id)sender
{
   // This is a custom method to retrieve the name of the PDF file
   NSString* pdfFileName = [self getPDFFileName];
 
   // Create the PDF context using the default page size of 612 x 792.
   UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectZero, nil);
 
   // Prepare the text and create a CTFramesetter to handle the layout.
   CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, (CFStringRef)textView.text, NULL);
   CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);
   if (!framesetter)
      return;
 
   // Set up some local variables.
   CFRange currentRange = CFRangeMake(0, 0);
   NSInteger currentPage = 0;
   BOOL done = NO;
 
   // Begin the main loop to create the individual pages
   do
   {
      // Mark the beginning of a new page.
      UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
 
      // Draw a page number at the bottom of each page
      currentPage++;
      [self drawPageNumber:currentPage];
 
      // Render the current page and update the current range to
      // point to the beginning of the next page.
      currentRange = [self renderPageWithTextRange:currentRange andFramesetter:framesetter];
 
      // If we're at the end of the text, exit the loop.
      if (currentRange.location == CFAttributedStringGetLength((CFAttributedStringRef)currentText))
         done = YES;
   }
   while (!done);
 
   // Close the PDF context and write the contents out.
   UIGraphicsEndPDFContext();
 
   // Clean up.
   CFRelease(currentText);
   CFRelease(framesetter);
}

Drawing PDF Pages

All PDF drawing must be done in the context of a page. Every PDF document has at least one page and many may have multiple pages. You specify the start of a new page by calling the UIGraphicsBeginPDFPage or UIGraphicsBeginPDFPageWithInfo function. These functions close the previous page (if one was open), create a new page, and prepare it for drawing. The UIGraphicsBeginPDFPage creates the new page using the default size while the UIGraphicsBeginPDFPageWithInfo function lets you customize the page size or customize other aspects of the PDF page.

After you create a page, all of your subsequent drawing commands are captured by the PDF graphics context and translated into PDF commands. You can draw anything you want in the page, including text, vector shapes, and images just as you would in your application’s custom views. The drawing commands you issue are captured by the PDF context and translated into PDF data. Placement of content on the the page is completely up to you but must take place within the bounding rectangle of the page.

Listing 6-8 shows two custom methods used to draw content inside a PDF page. The renderPageWithTextRange:andFramesetter: method uses Core Text to create a text frame that fits the page and then lay out some text inside that frame. After laying out the text, it returns an updated range that reflects the end of the current page and the beginning of the next page. The drawPageNumber: method uses the NSString drawing capabilities to draw a page number string at the bottom of each PDF page.

Listing 6-8  Drawing page-based content

// Use Core Text to draw the text in a frame on the page.
- (CFRange)renderPage:(NSInteger)pageNum withTextRange:(CFRange)currentRange
                 andFramesetter:(CTFramesetterRef)framesetter
{
   // Get the graphics context.
   CGContextRef    currentContext = UIGraphicsGetCurrentContext();
 
   // Put the text matrix into a known state. This ensures
   // that no old scaling factors are left in place.
   CGContextSetTextMatrix(context, CGAffineTransformIdentity);
 
   // Create a path object to enclose the text. Use 72 point
   // margins all around the text.
   CGRect    frameRect = CGRectMake(72, 72, 468, 648);
   CGMutablePathRef framePath = CGPathCreateMutable();
   CGPathAddRect(framePath, NULL, frameRect);
 
   // Get the frame that will do the rendering.
   // The currentRange variable specifies only the starting point. The framesetter
   // lays out as much text as will fit into the frame.
   CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
   CGPathRelease(framePath);
 
   // Core Text draws from the bottom-left corner up, so flip
   // the current transform prior to drawing.
   CGContextTranslateCTM(currentContext, 0, 792);
   CGContextScaleCTM(currentContext, 1.0, -1.0);
 
   // Draw the frame.
   CTFrameDraw(frameRef, currentContext);
 
   // Update the current range based on what was drawn.
   currentRange = CTFrameGetVisibleStringRange(frameRef);
   currentRange.location += currentRange.length;
   currentRange.length = 0;
   CFRelease(frameRef);
 
   return currentRange;
}
 
- (void)drawPageNumber:(NSInteger)pageNum
{
   NSString* pageString = [NSString stringWithFormat:@"Page %d", pageNum];
   UIFont* theFont = [UIFont systemFontOfSize:12];
   CGSize maxSize = CGSizeMake(612, 72);
 
   CGSize pageStringSize = [pageString sizeWithFont:theFont
                                       constrainedToSize:maxSize
                                       lineBreakMode:UILineBreakModeClip];
   CGRect stringRect = CGRectMake(((612.0 - pageStringSize.width) / 2.0),
                                  720.0 + ((72.0 - pageStringSize.height) / 2.0) ,
                                  pageStringSize.width,
                                  pageStringSize.height);
 
   [pageString drawInRect:stringRect withFont:theFont];
}

Creating Links Within Your PDF Content

Besides drawing content, you can also include links that take the user to another page in the same PDF file or to an external URL. To create a single link, you must add a source rectangle and a link destination to your PDF pages. One of the attributes of the link destination is a string that serves as the unique identifier for that link. To create a link to a specific destination, you specify the unique identifier for that destination when creating the source rectangle.

To add a new link destination to your PDF content, you use the UIGraphicsAddPDFContextDestinationAtPoint function. This function associates a named destination with a specific point on the current page. When you want to link to that destination point, you use UIGraphicsSetPDFContextDestinationForRect function to specify the source rectangle for the link. Figure 6-4 shows the relationship between these two function calls when applied to the pages of your PDF documents. Tapping on the rectangle surrounding the “see Chapter 1” text takes the user to the corresponding destination point, which is located at the top of Chapter 1.

Figure 6-4  Creating a link destination and jump point

In addition to creating links within a document, you can also use the UIGraphicsSetPDFContextURLForRect function to create links to content located outside of the document. When using this function to create links, you do not need to create a link destination first. All you have to do is use this function to specify the target URL and the source rectangle on the current page.




Last updated: 2010-04-13

Did this document help you? Yes It's good, but... Not helpful...