Path Basics - iOS Drawing: Practical UIKit Solutions (2014)

iOS Drawing: Practical UIKit Solutions (2014)

Chapter 4. Path Basics

Bezier paths are some of the most important tools for iOS drawing, enabling you to create and draw shapes, establish clipping paths, define animation paths, and more. Whether you’re building custom view elements, adding Photoshop-like special effects, or performing ordinary tasks like drawing lines and circles, a firm grounding in the UIBezierPath class will make your development easier and more powerful.

Why Bezier

When building buttons or bevels, shadows or patterns, Bezier paths provide a powerful and flexible solution. Consider Figure 4-1. Both screenshots consist of elements sourced primarily from Bezier path drawing.

Image

Figure 4-1 Despite the striking visual differences between these two screenshots, the UIBezierPath class is primarily responsible for the content in both of them.

The top screenshot is from an audio sampling app. Its blue and white graph and its black audio scan are simple path instances drawn as solid and dashed lines. These lines demonstrate a conventional use case, where each path traces some pattern.

The bottom screenshot uses more sophisticated effects. The bunny’s geometry is stored in a single Bezier path. So is the white rounded rectangle frame that surrounds it. The gradient overlay on top of this button is clipped to a Bezier-based ellipse. Bezier paths also established the clipping paths that shape the outer edges and that define the limits for the subtle outer glow effect as well as the inner 3D beveling.

Although the results are strikingly different between the two screenshots, the class behind the drawing is the same. If you plan to incorporate UIKit drawing into your apps, you’ll want to develop a close and comfortable relationship with the UIBezierPath class.

Class Convenience Methods

UIBezierPath class methods build rectangles, ovals, rounded rectangles, and arcs, offering single-call access to common path style elements:

Image Rectangles—bezierPathWithRect: is used for any kind of view filling or rectangular elements you have in your interface. You can create any kind of rectangle with this method.

Image Ovals and circles—bezierPathWithOvalInRect: provides a tool for building circles and ellipses of any kind.

Image Rounded rectangles—bezierPathWithRoundedRect:cornerRadius: builds the rounded rectangles that iOS designers love so much. These paths are perfect for buttons, popup alerts, and many other view elements.

Image Corner-controlled rounded rectangles—bezierPathWithRoundedRect: byRoundingCorners:cornerRadii: enables you to create rounded rectangles but choose which corners to round. This is especially helpful for creating alerts and panels that slide in from just one side of the screen, where you don’t want to round all four corners.

Image Arcs—bezierPathWithArcCenter:radius:startAngle: endAngle:clockwise: draws arcs from a starting and ending angle that you specify. Use these arcs to add visual embellishments, to create stylized visual dividers, and to soften your overall GUI.

These Bezier path class methods provide a basic starting point for your drawing, as demonstrated in Example 4-1. This code builds a new Bezier path by combining several shapes. At each step, it appends the shape to an ever-growing path. When stroked, this resulting path produces the “face” shown in Figure 4-2.

Image

Figure 4-2 This face consists of a single Bezier path. It was constructed by appending class-supplied shapes, namely an oval (aka a circle), an arc, and two rectangles, to a path instance.

As you see in Example 4-1, Bezier path instances are inherently mutable. You can add to them and grow them by appending new shapes and elements. What’s more, paths need not be contiguous. The path in Figure 4-2 consists of four distinct, nonintersecting subshapes, which can be painted as a single unit.


Note

This is the first part of the book that uses examples. As the name suggests, examples show samples of applying API calls as you draw. Listings, in contrast, offer utilities and useful snippets that you may bring back to your own projects.


Although you will run into performance issues if your paths get too complex, this usually happens only with user-driven interaction. I represent drawings in my Draw app as paths. Kindergartners, with nothing better to do than draw continuously for 5 or 10 minutes at a time without ever lifting their finger, will cause slow-downs as paths become insanely long. You shouldn’t encounter this issue for any kind of (sane) path you prebuild using normal vector drawing and incorporate into your own applications.

You will want to break down paths, however, for logical units. Paths are drawn at once. A stroke width and fill/stroke colors are applied to all elements. When you need to add multiple drawing styles, you should create individual paths, each of which represents a single unified operation.

Example 4-1 Building a Bezier Path


CGRect fullRect = (CGRect){.size = size};

// Establish a new path
UIBezierPath *bezierPath = [UIBezierPath bezierPath];

// Create the face outline
// and append it to the path
CGRect inset = CGRectInset(fullRect, 32, 32);
UIBezierPath *faceOutline =
[UIBezierPath bezierPathWithOvalInRect:inset];
[bezierPath appendPath:faceOutline];

// Move in again, for the eyes and mouth
CGRect insetAgain = CGRectInset(inset, 64, 64);

// Calculate a radius
CGPoint referencePoint =
CGPointMake(CGRectGetMinX(insetAgain),
CGRectGetMaxY(insetAgain));
CGPoint center = RectGetCenter(inset);
CGFloat radius = PointDistanceFromPoint(referencePoint, center);

// Add a smile from 40 degrees around to 140 degrees
UIBezierPath *smile =
[UIBezierPath bezierPathWithArcCenter:center
radius:radius startAngle:RadiansFromDegrees(140)
endAngle:RadiansFromDegrees(40) clockwise:NO];
[bezierPath appendPath:smile];

// Build Eye 1
CGPoint p1 = CGPointMake(CGRectGetMinX(insetAgain),
CGRectGetMinY(insetAgain));
CGRect eyeRect1 = RectAroundCenter(p1, CGSizeMake(20, 20));
UIBezierPath *eye1 =
[UIBezierPath bezierPathWithRect:eyeRect1];
[bezierPath appendPath:eye1];

// And Eye 2
CGPoint p2 = CGPointMake(CGRectGetMaxX(insetAgain),
CGRectGetMinY(insetAgain));
CGRect eyeRect2 = RectAroundCenter(p2, CGSizeMake(20, 20));
UIBezierPath *eye2 =
[UIBezierPath bezierPathWithRect:eyeRect2];
[bezierPath appendPath:eye2];

// Draw the complete path
bezierPath.lineWidth = 4;
[bezierPath stroke];


Nesting Rounded Rectangles

Figure 4-3 shows two nested rounded rectangles. Building rounded rectangles is a common drawing task in iOS apps. Each path is filled and stroked: the outer rectangle with a 2-point line and the inner rectangle with a 1-point line. Figure 4-3 demonstrates a common error.

Image

Figure 4-3 Nested rectangles with rounded corners.

Look carefully at the corners of each image. The inner path’s corner radius differs between the first image and second image. The left image insets its inner path by 4 points and draws it using the same 12-point corner radius as the first path:

UIBezierPath *path2 = [UIBezierPath bezierPathWithRoundedRect:
CGRectInset(destinationRect, 4, 4) cornerRadius:12];

The second performs the same 4-point inset but reduces the corner radius from 12 points to 8. Matching the inset to the corner radius creates a better fit between the two paths, ensuring that both curves start and end at parallel positions. It produces the more visually pleasing result you see in the right-hand image.

Building Paths

When system-supplied paths like rectangles and ovals are insufficient to your needs, you can iteratively build paths. You create paths by laying out items point-by-point, adding curves and lines as you go.

Each Bezier path can include a variety of geometric elements, including the following:

Image Positioning requests established by moveToPoint:.

Image Straight lines, added by addLineToPoint:.

Image Cubic Bezier curve segments created by addCurveToPoint:controlPoint1:controlPoint2:.

Image Quadratic Bezier curve segments built by addQuadCurveToPoint:controlPoint:.

Image Arcs added with calls to addArcToCenter:radius:startAngle: endAngle:clockwise:.

Figure 4-4 shows a star built from a series of cubic segments. Example 4-2 details the code behind the drawing. It establishes a new path, tells it to move to the starting point (p0), and then adds a series of cubic curves to build the star shape.

Image

Figure 4-4 This shape started off in Photoshop. Pixel Cut’s PaintCode transformed it into the code in Example 4-2.

If this code seems particularly opaque, well, it is. Constructing a Bezier curve is not a code-friendly process. In fact, this particular path began its life in Photoshop. I drew a shape and saved it to a PSD file. Then I used PaintCode (available from the Mac App Store for $99 + $20 in-app Photoshop file import purchase) to transform the vector art into a series of Objective-C calls.

When it comes to drawing, the tools best suited to expressing shapes often lie outside Xcode. After designing those shapes, however, there’s a lot you can do from code to manage and develop that material for your app. For that reason, you needn’t worry about the exact offsets, positions, and sizes you establish in Photoshop or other tools. As you’ll discover, UIBezierPath instances express vector art. They can be scaled, translated, rotated, and more, all at your command, so you can tweak that material exactly as needed in your apps.

Example 4-2 Creating a Star Path


// Create new path, courtesy of PaintCode (paintcodeapp.com)
UIBezierPath* bezierPath = [UIBezierPath bezierPath];

// Move to the start of the path, at the right side
[bezierPath moveToPoint: CGPointMake(883.23, 430.54)]; // p0

// Add the cubic segments
[bezierPath addCurveToPoint: CGPointMake(749.25, 358.4) // p1
controlPoint1: CGPointMake(873.68, 370.91)
controlPoint2: CGPointMake(809.43, 367.95)];
[bezierPath addCurveToPoint: CGPointMake(668.1, 353.25) // p2
controlPoint1: CGPointMake(721.92, 354.07)
controlPoint2: CGPointMake(690.4, 362.15)];
[bezierPath addCurveToPoint: CGPointMake(492.9, 156.15) // p3
controlPoint1: CGPointMake(575.39, 316.25)
controlPoint2: CGPointMake(629.21, 155.47)];
[bezierPath addCurveToPoint: CGPointMake(461.98, 169.03) // p4
controlPoint1: CGPointMake(482.59, 160.45)
controlPoint2: CGPointMake(472.29, 164.74)];
[bezierPath addCurveToPoint: CGPointMake(365.36, 345.52) // p5
controlPoint1: CGPointMake(409.88, 207.98)
controlPoint2: CGPointMake(415.22, 305.32)];
[bezierPath addCurveToPoint: CGPointMake(262.31, 358.4) // p6
controlPoint1: CGPointMake(341.9, 364.44)
controlPoint2: CGPointMake(300.41, 352.37)];
[bezierPath addCurveToPoint: CGPointMake(133.48, 460.17) // p7
controlPoint1: CGPointMake(200.89, 368.12)
controlPoint2: CGPointMake(118.62, 376.61)];
[bezierPath addCurveToPoint: CGPointMake(277.77, 622.49) // p8
controlPoint1: CGPointMake(148.46, 544.36)
controlPoint2: CGPointMake(258.55, 560.05)];
[bezierPath addCurveToPoint: CGPointMake(277.77, 871.12) // p9
controlPoint1: CGPointMake(301.89, 700.9)
controlPoint2: CGPointMake(193.24, 819.76)];
[bezierPath addCurveToPoint: CGPointMake(513.51, 798.97) // p10
controlPoint1: CGPointMake(382.76, 934.9)
controlPoint2: CGPointMake(435.24, 786.06)];
[bezierPath addCurveToPoint: CGPointMake(723.49, 878.84) // p11
controlPoint1: CGPointMake(582.42, 810.35)
controlPoint2: CGPointMake(628.93, 907.89)];
[bezierPath addCurveToPoint: CGPointMake(740.24, 628.93) // p12
controlPoint1: CGPointMake(834.7, 844.69)
controlPoint2: CGPointMake(722.44, 699.2)];
[bezierPath addCurveToPoint: CGPointMake(883.23, 430.54) // p0
controlPoint1: CGPointMake(756.58, 564.39)
controlPoint2: CGPointMake(899.19, 530.23)];


Drawing Bezier Paths

After you create Bezier path instances, you draw them to a context by applying fill or stroke. Filling paints a path, adding color to all the areas inside the path. Stroking outlines a path, painting just the edges, using the line width stored in the path’s lineWidth property. A typical drawing pattern might go like this:

myPath.lineWidth = 4.0f;
[[UIColor blackColor] setStroke];
[[UIColor redColor] setFill];
[myPath fill];
[myPath stroke];

This sequence assigns a line width to the path, sets the fill and stroke colors for the current context, and then fills and strokes the path.

Listing 4-1 introduces what I consider to be a more convenient approach. The methods define a class category that adds two new ways to draw. This category condenses the drawing sequence as follows:

[myPath fill:[UIColor redColor]];
[myPath stroke:4 color:[UIColor blackColor]];

The colors and stroke width become parameters of a single drawing request. These functions enable you to create a series of drawing operations without having to update context or instance properties between requests.

All drawing occurs within the graphics state (GState) save and restore requests introduced in Chapter 1. This ensures that the passed parameters don’t persist past the method call, so you can return to exactly the same context and path state you left.


Note

When working with production code (that is, when not writing a book and trying to offer easy-to-read examples), please make sure to namespace your categories. Add custom prefixes that guarantee your extensions won’t overlap with any possible future Apple class updates. esStroke:color: isn’t as pretty as stroke:color:, but it offers long-term protection for your code.


Listing 4-1 Drawing Utilities


@implementation UIBezierPath (HandyUtilities)
// Draw with width
- (void) stroke: (CGFloat) width color: (UIColor *) color
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to draw to");
return;
}

CGContextSaveGState(context);

// Set the color
if (color) [color setStroke];

// Store the width
CGFloat holdWidth = self.lineWidth;
self.lineWidth = width;

// Draw
[self stroke];

// Restore the width
self.lineWidth = holdWidth;
CGContextRestoreGState(context);
}

// Fill with supplied color
- (void) fill: (UIColor *) fillColor
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to draw to");
return;
}
CGContextSaveGState(context);
[fillColor set];
[self fill];
CGContextRestoreGState(context);
}


Drawing Inside Paths

In UIKit, the UIRectFrame() function draws a single line inside a rectangle you supply as a parameter. This function provides particularly clean and appealing results. It was my inspiration for Listing 4-2.

Listing 4-2 performs the same stroke operation that you saw in Figure 4-3 but with two slight changes. First, it doubles the size of the stroke. Second, it clips the drawing using addClip.

Normally, a stroke operation draws the stroke in the center of the path’s edge. This creates a stroke that’s half on one side of that edge and half on the other. Doubling the size ensures that the inside half of the stroke uses exactly the size you specified.

As you discovered in Chapter 1, clipping creates a mask. It excludes material from being added to the context outside the clipped bounds. In Listing 4-2, clipping prevents the stroke from continuing past the path’s edge, so all drawing occurs inside the path. The result is a stroke of a fixed size that’s drawn fully within the path.

Listing 4-2 Stroking Inside a Path


- (void) strokeInside: (CGFloat) width color: (UIColor *) color
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to draw to");
return;
}

CGContextSaveGState(context);
[self addClip];
[self stroke:width * 2 color:color]; // Listing 4-1
CGContextRestoreGState(context);
}


Filling Paths and the Even/Odd Fill Rule

In vector graphics, winding rules establish whether areas are inside or outside a path. These rules affect whether areas, like those shown in Figure 4-5, are colored or not when you fill a path. Quartz uses an even/odd rule, which is an algorithm that determines the degree to which any area is “inside” a path.

Image

Figure 4-5 An even/odd fill rule establishes whether Quartz fills the entire inner path or only “inside” areas. The shapes on the left side use the default fill rule. The shapes on the right apply the even/odd fill rule.

You use this rule in many drawing scenarios. For example, the even/odd fill rule enables you to draw complex borders. It offers a way to cut areas out of your text layouts so you can insert images. It provides a basis for inverting selections, enabling you to create positive and negative space and more.

The algorithm tests containment by projecting a ray (a line with one fixed end, pointing in a given direction) from points within the path to a distant point outside it. The algorithm counts the number of times that ray crosses any line. If the ray passes through an even number of intersections, the point is outside the shape; if odd, inside.

In the nested square example at the left of Figure 4-5, a point in the very center passes through four subsequent lines before passing out of the path. A point lying just past that box, between the third and fourth inner squares, would pass through only three lines on the way out. Therefore, according to the even/odd fill rule, the first point is “outside” the shape because it passes through an even number of lines. The second point is “inside” the shape because it passes through an odd number.

These even and odd numbers have consequence, which you can see in Figure 4-5. When you enable a path’s usesEvenOddFillRule property, UIKit uses this calculation to determine which areas lay inside the shape and should be filled and what areas are outside and should not.

A special version of the context fill function in Quartz, CGContextEOFillPath(), performs a context fill by applying the even/odd fill rule. When you use the normal version of the function, CGContextFillPath(), the rule is not applied.

Retrieving Path Bounds and Centers

The UIBezierPath class includes a bounds property. It returns a rectangle that fully encloses all points in the path, including control points. Think of this property as a path’s frame. The origin almost never is at zero; instead, it represents the top-most and left-most point, or the control point used to construct the path.

Bounds are useful because they’re fast to calculate and provide a good enough approximation for many drawing tasks. Figure 4-6 demonstrates an inherent drawback. When you’re working with an exacting layout, bounds do not take the true extent of a figure’s curves into account. For that reason, bounds almost always exceed actual boundary extents.

Image

Figure 4-6 The green rendering shows the path. Purple circles are the control points used to construct that path. The dashed rectangles indicate the path’s bounds property (outer rectangle) and the true boundary (inner rectangle). Although close in size, they are not identical, and that difference can be important when you’re creating perfect drawings.

Listing 4-3 turns to the Quartz CGPathGetPathBoundingBox() function to retrieve better path bounds. This approach demands more calculation as the function must interpolate between control points to establish the points along each curve. The results are far superior. You trade accuracy for the overhead of time and processor demands.

The PathBoundingBox() function returns the closer bounds shown in Figure 4-6’s inner rectangle. A related function, PathBoundingBoxWithLineWidth(), extends those bounds to take a path’s lineWidth property into account. Strokes are drawn over a path’s edges, so the path generally expands by half that width in each direction.

Listing 4-3 also includes functions to calculate a path’s center, using both the general (PathCenter()) and precise (PathBoundingCenter()) results. The latter version offers a better solution for transformations, where precision matters. (Slight errors can be magnified by affine transforms.) Using the path bounding box versions of these measurements ensures a more consistent end result.


Note

In my own development, I’ve implemented the routines in Listing 4-3 as a UIBezierPath category.


Listing 4-3 A Path’s Calculated Bounds


// Return calculated bounds
CGRect PathBoundingBox(UIBezierPath *path)
{
return CGPathGetPathBoundingBox(path.CGPath);
}

// Return calculated bounds taking line width into account
CGRect PathBoundingBoxWithLineWidth(UIBezierPath *path)
{
CGRect bounds = PathBoundingBox(path);
return CGRectInset(bounds,
-path.lineWidth / 2.0f, -path.lineWidth / 2.0f);
}

// Return the calculated center point
CGPoint PathBoundingCenter(UIBezierPath *path)
{
return RectGetCenter(PathBoundingBox(path));
}

// Return the center point for the bounds property
CGPoint PathCenter(UIBezierPath *path)
{
return RectGetCenter(path.bounds);
}


Transforming Paths

The Bezier path’s applyTransform: method transforms all of a path’s points and control points, by applying the affine transform matrix passed as the method argument. This change happens in place and will update the calling path. For example, after you apply the following scale transform, the entire myPath path shrinks:

[myPath applyTransform:CGAffineTransformMakeScale(0.5f, 0.5f)];

If you’d rather preserve the original path, create a copy and apply the transform to the copy. This enables you to perform multiple changes that leave the original item intact:

UIBezierPath *pathCopy = [myPath copy];
[pathCopy applyTransform: CGAffineTransformMakeScale(0.5f, 0.5f)];

You cannot “revert” a path by applying an identity transform. It produces a “no op” result (the path stays the same) rather than a reversion (the path returns to what it originally was).

The Origin Problem

Apply transforms don’t always produce the results you expect. Take Figure 4-7, for example. I created a path and applied a rotation to it:

[path applyTransform:CGAffineTransformMakeRotation(M_PI / 9)];

Image

Figure 4-7 A rotation whose origin is not explicitly set to a path’s center produces unexpected results.

The left image in Figure 4-7 shows what you might expect to happen, where the image rotates 20 degrees around its center. The right image shows what actually happens when you do not control a transform’s point of origin. The path rotates around the origin of the current coordinate system.

Fortunately, it’s relatively easy to ensure that rotation and scaling occur the way you anticipate. Listing 4-4 details ApplyCenteredPathTransform(), a function that bookends an affine transform with translations that set and then, afterward, reset the coordinate system. These extra steps produce the controlled results you’re looking for.

Listing 4-4 Rotating a Path Around Its Center


// Translate path's origin to its center before applying the transform
void ApplyCenteredPathTransform(
UIBezierPath *path, CGAffineTransform transform)
{
CGPoint center = PathBoundingCenter(path);
CGAffineTransform t = CGAffineTransformIdentity;
t = CGAffineTransformTranslate(t, center.x, center.y);
t = CGAffineTransformConcat(transform, t);
t = CGAffineTransformTranslate(t, -center.x, -center.y);
[path applyTransform:t];
}

// Rotate path around its center
void RotatePath(UIBezierPath *path, CGFloat theta)
{
CGAffineTransform t =
CGAffineTransformMakeRotation(theta);
ApplyCenteredPathTransform(path, t);
}


Other Transformations

As with rotation, if you want to scale an object in place, make sure you apply your transform at the center of a path’s bounds. Figure 4-8 shows a transform that scales a path by 85% in each dimension. The function that created this scaling is shown in Listing 4-5, along with a number of other convenient transformations.

Image

Figure 4-8 The smaller green image is scaled at (0.85, 0.85) of the original.

Listings 4-4 and 4-5 implement many of the most common transforms you need for Quartz drawing tasks. In addition to the rotation and scaling options, these functions enable you to move a path to various positions and to mirror the item in place. Centered origins ensure that each transform occurs in the most conventionally expected way.

Listing 4-5 Applying Transforms


// Scale path to sx, sy
void ScalePath(UIBezierPath *path, CGFloat sx, CGFloat sy)
{
CGAffineTransform t = CGAffineTransformMakeScale(sx, sy);
ApplyCenteredPathTransform(path, t);
}

// Offset a path
void OffsetPath(UIBezierPath *path, CGSize offset)
{
CGAffineTransform t =
CGAffineTransformMakeTranslation(
offset.width, offset.height);
ApplyCenteredPathTransform(path, t);
}

// Move path to a new origin
void MovePathToPoint(UIBezierPath *path, CGPoint destPoint)
{
CGRect bounds = PathBoundingBox(path);
CGSize vector =
PointsMakeVector(bounds.origin, destPoint);
OffsetPath(path, vector);
}

// Center path around a new point
void MovePathCenterToPoint(
UIBezierPath *path, CGPoint destPoint)
{
CGRect bounds = PathBoundingCenter(path);
CGSize vector = PointsMakeVector(bounds.origin, destPoint);
vector.width -= bounds.size.width / 2.0f;
vector.height -= bounds.size.height / 2.0f;
OffsetPath(path, vector);
}

// Flip horizontally
void MirrorPathHorizontally(UIBezierPath *path)
{
CGAffineTransform t = CGAffineTransformMakeScale(-1, 1);
ApplyCenteredPathTransform(path, t);
}

// Flip vertically
void MirrorPathVertically(UIBezierPath *path)
{
CGAffineTransform t = CGAffineTransformMakeScale(1, -1);
ApplyCenteredPathTransform(path, t);
}


Fitting Bezier Paths

Listing 4-6 tackles one of the most important tasks for working with Bezier paths: working with a path created at an arbitrary point and scale, and drawing it into a specific rectangle. Fitting vector-based art ensures that you can reliably place items into your drawing with expected dimensions and destinations.

Listing 4-6 uses the same fitting approach you’ve seen used several times already in this book. The difference is that this version uses the scale and fitting rectangle to apply transforms to the path. The path is moved to its new center and then scaled in place down to the proper size.

Listing 4-6 Fitting a Path


void FitPathToRect(UIBezierPath *path, CGRect destRect)
{
CGRect bounds = PathBoundingBox(path);
CGRect fitRect = RectByFittingRect(bounds, destRect);
CGFloat scale = AspectScaleFit(bounds.size, destRect);

CGPoint newCenter = RectGetCenter(fitRect);
MovePathCenterToPoint(path, newCenter);
ScalePath(path, scale, scale);
}


Creating Bezier Paths from Strings

Core Text simplifies the process of transforming strings to Bezier paths. Listing 4-7 provides a simple conversion function. It transforms its string into individual Core Text glyphs, represented as individual CGPath items. The function adds each letter path to a resulting Bezier path, offsetting itself after each letter by the size of that letter.

After all the letters are added, the path is mirrored vertically. This converts the Quartz-oriented output into a UIKit-appropriate layout. You can treat these string paths just like any others, setting their line widths, filling them with colors and patterns, and transforming them however you like.Figure 4-9 shows a path created from a bold Baskerville font and filled with a green pattern.

Image

Figure 4-9 This is a filled and stroked Bezier path created by Listing 4-7 from an NSString instance.

Here’s the snippet that created that path:

UIFont *font = [UIFont fontWithName:@"Baskerville-Bold" size:16];
UIBezierPath *path = BezierPathFromString(@"Hello World", font);
FitPathToRect(path, targetRect);
[path fill:GreenStripesColor()];
[path strokeInside:4];

Interestingly, font size doesn’t play a role in this particular drawing. The path is proportionately scaled to the destination rectangle, so you can use almost any font to create the source.

If you want the path to look like normal typeset words, just fill the returned path with a black fill color without stroking. This green-filled example used an inside stroke to ensure that the edges of the type path remained crisp.

Listing 4-7 Creating Bezier Paths from Strings


UIBezierPath *BezierPathFromString(
NSString *string, UIFont *font)
{
// Initialize path
UIBezierPath *path = [UIBezierPath bezierPath];
if (!string.length) return path;

// Create font ref
CTFontRef fontRef = CTFontCreateWithName(
(__bridge CFStringRef)font.fontName,
font.pointSize, NULL);
if (fontRef == NULL)
{
NSLog(@"Error retrieving CTFontRef from UIFont");
return nil;
}

// Create glyphs (that is, individual letter shapes)
CGGlyph *glyphs = malloc(sizeof(CGGlyph) * string.length);
const unichar *chars = (const unichar *)[string
cStringUsingEncoding:NSUnicodeStringEncoding];
BOOL success = CTFontGetGlyphsForCharacters(
fontRef, chars, glyphs, string.length);
if (!success)
{
NSLog(@"Error retrieving string glyphs");
CFRelease(fontRef);
free(glyphs);
return nil;
}

// Draw each char into path
for (int i = 0; i < string.length; i++)
{
// Glyph to CGPath
CGGlyph glyph = glyphs[i];
CGPathRef pathRef =
CTFontCreatePathForGlyph(fontRef, glyph, NULL);

// Append CGPath
[path appendPath:[UIBezierPath
bezierPathWithCGPath:pathRef]]

// Offset by size
CGSize size =
[[string substringWithRange: NSMakeRange(i, 1)]
sizeWithAttributes:@{NSFontAttributeName:font}];
OffsetPath(path, CGSizeMake(-size.width, 0));
}

// Clean up
free(glyphs); CFRelease(fontRef);

// Return the path to the UIKit coordinate system
MirrorPathVertically(path);
return path;
}


Adding Dashes

UIKit makes it easy to add dashes to a Bezier path. For example, you might add a simple repeating pattern, like this:

CGFloat dashes[] = {6, 2};
[path setLineDash:dashes count:2 phase:0];

The array specifies, in points, the on/off patterns used when drawing a stroke. This example draws lines of 6 points followed by spaces of 2 points. Your dash patterns will naturally vary by the scale of the drawing context and any compression used by the view. That said, there are many ways to approach dashes. Table 4-1 showcases some basic options.

Image

Image

Table 4-1 Dash Patterns

The phase of a dash indicates the amount it is offset from the beginning of the drawing. In this example, the entire pattern is 8 points long. Moving the phase iteratively from 0 up to 7 enables you to animate the dashes, creating a marching ants result that surrounds a Bezier path. You will read about how to accomplish this effect in Chapter 7.

Building a Polygon Path

Although UIBezierPath offers easy ovals and rectangles, it does not provide a simple N-gon generator. Listing 4-8 fills that gap, returning Bezier paths with the number of sides you request. Some basic shapes it creates are shown in Figure 4-10.

Image

Figure 4-10 These polygon Bezier paths were generated by Listing 4-8. The shapes have, respectively, 3, 4, 5, and 8 sides.

This function creates its shapes by dividing a circle (2 pi radians) into the requested number of sides and then drawing lines from one destination point to the next. The final segment closes the path, avoiding drawing artifacts at the starting point of the shape. Figure 4-11 shows the kind of effect you’ll encounter when you don’t properly close your shape. Quartz treats that corner as two line segments instead of a proper join.

Image

Figure 4-11 Avoid stroking gaps by closing the path from the last point to the origin.

All returned paths use unit sizing—that is, they fit within the {0, 0, 1, 1} rectangle. You size the path as needed. Also, the first point is always placed here at the top of the shape, so if you intend to return a square instead of a diamond, make sure to rotate by 90 degrees.

Listing 4-8 Generating Polygons


UIBezierPath *BezierPolygon(NSUInteger numberOfSides)
{
if (numberOfSides < 3)
{
NSLog(@"Error: Please supply at least 3 sides");
return nil;
}

UIBezierPath *path = [UIBezierPath bezierPath];

// Use a unit rectangle as the destination
CGRect destinationRect = CGRectMake(0, 0, 1, 1);
CGPoint center = RectGetCenter(destinationRect);
CGFloat r = 0.5f; // radius

BOOL firstPoint = YES;
for (int i = 0; i < (numberOfSides – 1); i++)
{
CGFloat theta = M_PI + i * TWO_PI / numberOfSides;
CGFloat dTheta = TWO_PI / numberOfSides;

CGPoint p;
if (firstPoint)
{
p.x = center.x + r * sin(theta);
p.y = center.y + r * cos(theta);
[path moveToPoint:p];
firstPoint = NO;
}

p.x = center.x + rx * sin(theta + dTheta);
p.y = center.y + ry * cos(theta + dTheta);
[path addLineToPoint:p];
}

[path closePath];

return path;
}


Line Joins and Caps

A path’s lineJoinStyle property establishes how to draw the point at which each line meets another. Quartz offers three styles for you to work with, which are shown in Figure 4-12. The default, kCGLineJoinMiter, creates a sharp angle. You round those edges by choosingkCGLineJoinRound instead. The final style is kCGLineJoinBevel. It produces flat endcaps with squared-off ends.

Image

Figure 4-12 From left to right: mitered, rounded, and beveled joins.

Lines points that don’t join other lines have their own styles, called caps. Figure 4-13 shows the three possible cap styles. I’ve added gray vertical lines to indicate the natural endpoint of each line. The kCGLineCapButt style ends exactly with the line. In two of the three, however, the line ornament extends beyond the final point. This additional distance for kCGLineCapSquare and kCGLineCapRound is half the line’s width. The wider the line grows, the longer the line cap extends.

Image

Figure 4-13 From top to bottom: butt, square, and round line caps.

Miter Limits

Miter limits restrict the pointiness of a shape, as you see in Figure 4-14. When the diagonal length of a miter—that is, the triangular join between two lines—exceeds a path’s limit, Quartz converts those points into bevel joins instead.

Image

Figure 4-14 When shapes reach their miter limit, angled joins convert to flat bevels.

In the left image, the shape has sharp angles. Its pointy extensions have not reached its path’s miterLimit. Lowering the limit below the actual length of those points, as in the right image, forces those joins to be clipped.

The default Bezier path miter limit is 10 points. This affects any angle under 11 degrees. As you change the miter limit, the default cutoff point adjusts. For example, at 30 degrees, you’d have to lower the limit below 3.8 points in order to see any effect. Miters start growing most extremely in the 0- to 11-degree range. The natural miter that’s just 10.4 points long for an 11-degree angle becomes 23 points at 5 degrees and almost 60 points at 2 degrees. Adding reasonable limits ensures that as your angles grow small, miter lengths won’t overwhelm your drawings.

Appendix B offers a more in-depth look at the math behind the miters.

Inflected Shapes

Figure 4-14 shows an example of a simple shape built by adding cubic curves around a center point. It was created by using the same function that built the shapes you see in Figure 4-15.

Image

Figure 4-15 Inflection variations: The fourth and fifth shapes shown here use large negative inflections that cross the center. Top row: 8 points and –0.5 inflection, 12 points and –0.75 inflection, 8 points and 0.5 inflection. Bottom row: 8 points and –2.5 inflection, 12 points and –2.5 inflection, 12 points and 2.5 inflection.

If you think the code in Listing 4-9 looks very similar to the polygon generator in Listing 4-8, you’re right. The difference between the two functions is that Listing 4-9 creates a curve between its points instead of drawing a straight line. You specify how many curves to create.

The inflection of that curve is established by two control points. These points are set by the percentInflection parameter you pass to the function. Positive inflections move further away from the center, building lobes around the shape. Negative inflections move toward the center—or even past the center—creating the spikes and loops you see on the other figures.

Listing 4-9 Generating Inflected Shapes


UIBezierPath *BezierInflectedShape(
NSUInteger numberOfInflections,
CGFloat percentInflection)
{
if (numberOfInflections < 3)
{
NSLog(@"Error: Please supply at least 3 inflections");
return nil;
}

UIBezierPath *path = [UIBezierPath bezierPath];
CGRect destinationRect = CGRectMake(0, 0, 1, 1);
CGPoint center = RectGetCenter(destinationRect);
CGFloat r = 0.5;
CGFloat rr = r * (1.0 + percentInflection);

BOOL firstPoint = YES;
for (int i = 0; i < numberOfInflections; i++)
{
CGFloat theta = i * TWO_PI / numberOfInflections;
CGFloat dTheta = TWO_PI / numberOfInflections;

if (firstPoint)
{
CGFloat xa = center.x + r * sin(theta);
CGFloat ya = center.y + r * cos(theta);
CGPoint pa = CGPointMake(xa, ya);
[path moveToPoint:pa];
firstPoint = NO;
}

CGFloat cp1x = center.x + rr * sin(theta + dTheta / 3);
CGFloat cp1y = center.y + rr * cos(theta + dTheta / 3);
CGPoint cp1 = CGPointMake(cp1x, cp1y);

CGFloat cp2x = center.x + rr *
sin(theta + 2 * dTheta / 3);
CGFloat cp2y = center.y + rr *
cos(theta + 2 * dTheta / 3);
CGPoint cp2 = CGPointMake(cp2x, cp2y);

CGFloat xb = center.x + r * sin(theta + dTheta);
CGFloat yb = center.y + r * cos(theta + dTheta);
CGPoint pb = CGPointMake(xb, yb);

[path addCurveToPoint:pb
controlPoint1:cp1 controlPoint2:cp2];
}

[path closePath];
return path;
}


By tweaking Listing 4-9 to draw lines instead of curves, you create star shapes instead. In Listing 4-10, a single control point placed halfway between each point provides a destination for the angles of a star. Figure 4-16 highlights some shapes you can produce using Listing 4-10.

Image

Figure 4-16 An assortment of stars built with Listing 4-10. Top row: 5 points and 0.75 inflection, 8 points and 0.5 inflection, 9 points and –2 inflection. Bottom row: 6 points and –1.5 inflection, 12 points and 0.75 inflection (filled). The last item, which uses the even/odd fill rule, was built with BezierStarShape(12, -2.5).

Listing 4-10 Generating Star Shapes


UIBezierPath *BezierStarShape(
NSUInteger numberOfInflections, CGFloat percentInflection)
{
if (numberOfInflections < 3)
{
NSLog(@"Error: Please supply at least 3 inflections");
return nil;
}

UIBezierPath *path = [UIBezierPath bezierPath];
CGRect destinationRect = CGRectMake(0, 0, 1, 1);
CGPoint center = RectGetCenter(destinationRect);
CGFloat r = 0.5;
CGFloat rr = r * (1.0 + percentInflection);

BOOL firstPoint = YES;
for (int i = 0; i < numberOfInflections; i++)
{
CGFloat theta = i * TWO_PI / numberOfInflections;
CGFloat dTheta = TWO_PI / numberOfInflections;

if (firstPoint)
{
CGFloat xa = center.x + r * sin(theta);
CGFloat ya = center.y + r * cos(theta);
CGPoint pa = CGPointMake(xa, ya);
[path moveToPoint:pa];
firstPoint = NO;
}

CGFloat cp1x = center.x + rr *
sin(theta + dTheta / 2);
CGFloat cp1y = center.y + rr *
cos(theta + dTheta / 2);
CGPoint cp1 = CGPointMake(cp1x, cp1y);

CGFloat xb = center.x + r * sin(theta + dTheta);
CGFloat yb = center.y + r * cos(theta + dTheta);
CGPoint pb = CGPointMake(xb, yb);

[path addLineToPoint:cp1];
[path addLineToPoint:pb];
}

[path closePath];
return path;
}


Summary

This chapter introduces the UIBezierPath class. It provides a basic overview of the class’s role and the properties that you can adjust. You saw how to fill and stroke paths, as well as how to construct complex shapes. You read about building new instances in both conventional and unexpected ways. Before moving on to more advanced path topics, here are some final thoughts to take from this chapter:

Image Paths provide a powerful tool for both representing geometric information and drawing that material into your apps. They create a vector-based encapsulation of a shape that can be used at any position, at any size, and at any rotation you need. Their resolution independence means they retain sharpness and detail, no matter what scale they’re drawn at.

Image When you turn strings into paths, you can have a lot of fun with text. From filling text with colors to playing with individual character shapes, there’s a wealth of expressive possibilities for you to take advantage of.

Image Take care when using bounding box calculations to estimate drawing bounds. Items like miters and line caps may extend beyond a path’s base bounds when drawn.