Transforms
The sequence of letters in Figure 1-13 is built by drawing each character at points around a circle. This figure takes advantage of one of UIKit’s terrific built-in features: NSString instances know how to draw themselves into contexts. You just tell a string to draw at a point or into a rectangle, as in this example:
[@"Hello" drawAtPoint:CGPointMake(100, 50) withAttributes:@{NSFontAttributeName:font}]
The syntax for this call changed in iOS 7, deprecating earlier APIs. Prior to iOS 7, you’d say:
[@"Hello" drawAtPoint:CGPointMake(100, 50) withFont:font]
Figure 1-13 Drawing letters in a circle.
This circle progresses clockwise, with a letter deposited every (2 × Pi / 26) radians. Each x and y position was calculated as an offset from a common center. The following code iteratively calculates points around the circle using r × sin(theta) and r × cos(theta), using those points to place each letter:
NSString *alphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; for (int i = 0; i < 26; i++) { NSString *letter = [alphabet substringWithRange:NSMakeRange(i, 1)]; CGSize letterSize = [letter sizeWithAttributes:@{NSFontAttributeName:font}]; CGFloat theta = M_PI - i * (2 * M_PI / 26.0); CGFloat x = center.x + r * sin(theta) - letterSize.width / 2.0; CGFloat y = center.y + r * cos(theta) - letterSize.height / 2.0; [letter drawAtPoint:CGPointMake(x, y) withAttributes:@{NSFontAttributeName:font}]; }
This is an acceptable way to approach this challenge, but you could greatly improve it by leveraging context transforms.
Transform State
Every context stores a 2D affine transform as part of its state. This transform is called the current transform matrix. It specifies how to rotate, translate, and scale the context while drawing. It provides a powerful and flexible way to create advanced drawing operations. Contrast the layout shown in Figure 1-14 with the one in Figure 1-13. This improvement is achieved through the magic of context transforms.
Figure 1-14 Drawing letters in a circle using transforms.
Listing 1-12 shows the steps that went into creating this. It consists of a series of transform operations that rotate the canvas and draw each letter. Context save and restore operations ensure that the only transform that persists from one drawing operation to the next is the one that appears in boldface in the listing. This translation sets the context’s origin to its center point.
This enables the context to freely rotate around that point, so each letter can be drawn at an exact radius. Moving to the left by half of each letter width ensures that every letter is drawn centered around the point at the end of that radius.
Listing 1-12 Transforming Contexts During Drawing
NSString *alphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // Start drawing UIGraphicsBeginImageContext(bounds.size); CGContextRef context = UIGraphicsGetCurrentContext(); // Retrieve the center and set a radius CGPoint center = RectGetCenter(bounds); CGFloat r = center.x * 0.75f; // Start by adjusting the context origin // This affects all subsequent operations CGContextTranslateCTM(context, center.x, center.y); // Iterate through the alphabet for (int i = 0; i < 26; i++) { // Retrieve the letter and measure its display size NSString *letter = [alphabet substringWithRange:NSMakeRange(i, 1)]; CGSize letterSize = [letter sizeWithAttributes:@{NSFontAttributeName:font}]; // Calculate the current angular offset CGFloat theta = i * (2 * M_PI / (float) 26); // Encapsulate each stage of the drawing CGContextSaveGState(context); // Rotate the context CGContextRotateCTM(context, theta); // Translate up to the edge of the radius and move left by // half the letter width. The height translation is negative // as this drawing sequence uses the UIKit coordinate system. // Transformations that move up go to lower y values. CGContextTranslateCTM(context, -letterSize.width / 2, -r); // Draw the letter and pop the transform state [letter drawAtPoint:CGPointMake(0, 0) withAttributes:@{NSFontAttributeName:font}]; CGContextRestoreGState(context); } // Retrieve and return the image UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image;
Building a More Exacting Layout
A more pedantic solution to drawing the letters in a circle avoids the extra spacing around the I and the squeezing around the W. Listing 1-13 details the steps needed to create the more precise layout shown in Figure 1-15. This example demonstrates finer layout placement.
Figure 1-15 This version uses better spacing, based on the width of each letter.
Start by calculating the total width of the layout. Sum the width of each individual letter (as done here) or just measure the string as a whole. This enables you to mark your progress along the layout, producing a percentage of travel from beginning to end.
Next, adjust the placement for each letter based on the percentage consumed by each iteration. Use this percentage to calculate a rotation angle for laying down the letter.
Finish by drawing the letter just as you did in Listing 1-12. What you end up with is a layout that respects the width of each letter, moving forward proportionately according to the letter’s natural size.
Listing 1-13 Precise Text Placement Around a Circle
// Calculate the full extent CGFloat fullSize = 0; for (int i = 0; i < 26; i++) { NSString *letter = [alphabet substringWithRange:NSMakeRange(i, 1)]; CGSize letterSize = [letter sizeWithAttributes:@{NSFontAttributeName:font}]; fullSize += letterSize.width; } // Initialize the consumed space CGFloat consumedSize = 0.0f; // Iterate through each letter, consuming that width for (int i = 0; i < 26; i++) { // Measure each letter NSString *letter = [alphabet substringWithRange:NSMakeRange(i, 1)]; CGSize letterSize = [letter sizeWithAttributes:@{NSFontAttributeName:font}]; // Move the pointer forward, calculating the // new percentage of travel along the path consumedSize += letterSize.width / 2.0f; CGFloat percent = consumedSize / fullSize; CGFloat theta = percent * 2 * M_PI; consumedSize += letterSize.width / 2.0f; // Prepare to draw the letter by saving the state CGContextSaveGState(context); // Rotate the context by the calculated angle CGContextRotateCTM(context, theta); // Move to the letter position CGContextTranslateCTM(context, -letterSize.width / 2, -r); // Draw the letter [letter drawAtPoint:CGPointMake(0, 0) withFont:font]; // Reset the context back to the way it was CGContextRestoreGState(context); }