Chapter 26: Fancy Text Layout

There are several options for displaying text in iOS. There are simple controls like UILabel and full rendering engines like UIWebView. For complete control, including laying out columns or drawing text along a path, you can implement your own layout engine with Core Text.

iOS 6 brings rich text to all of UIKit. Finally, you can display and edit bold and underline without using a web view or Core Text, which greatly simplifies many programs. To get the most out of iOS rich text, you need to understand NSAttributedString as explained in this chapter. After you understand attributed strings, handling rich text is mostly just a matter of requesting it from your text views.

After you’ve mastered attributed strings, you can take full control of text layout using Core Text. In this chapter, you learn about the major parts of Core Text, how to use them to lay out text any way you like, and the limitations that you still need to work around.

The Normal Stuff: Fields, Views, and Labels

You’re probably already familiar with UILabel, UITextField, and UITextView, so I discuss them only briefly here. They’re the basic controls you use for day-to-day text layout.

UILabel is a lightweight, static text control. It’s very common for developers to have problems with dynamic labels (labels with contents set programmatically) they create in Interface Builder. Here are a few tips for using UILabel:

UILabel can display multiline text. Just set its numberOfLines property in code or the Lines property in Interface Builder. This sets the maximum number of lines. If you want the number of lines to be unbounded, set it to zero.

By default, Interface Builder turns on adjustsFontSizeToFitWidth. This can be surprising if you assign text that’s wider than the label. Rather than truncating or overflowing the label, the text shrinks. Generally, it’s a good idea to make your dynamic labels very wide to avoid truncating or resizing.

Unlike other views, user interaction is disabled by default for UILabel. If you attach a UIGestureRecognizer to a UILabel, you must remember to set userInteractionEnabled to YES. Don’t confuse this with the enabled property, which controls only the appearance of the label.

UILabel is not a UIControl and doesn’t have a contentVerticalAlignment property. Text is always vertically centered. If you want to adjust the vertical location of the text, you need to resize the label with sizeToFit and then adjust the label’s frame.origin.

UITextField provides simple, single-line text entry. It includes an optional “clear” button and optional overlay views on the left and right (leftView and rightView). The overlay views can be used to provide hints about the field’s purpose. For instance, a search icon on the left is a very space-efficient way of indicating a search field. Remember that the overlay views are UIView objects. You can use a UIButton here or any other kind of interactive view. Just make sure they’re large enough for the user to touch easily. You generally should not move these views with setFrame:. Override the methods leftViewRectForBounds: or rightViewRectForBounds:. UITextField then lays them out appropriately. The text rectangle (textRectForBounds:) is clipped automatically, so it doesn’t overlap these rectangles.

A common problem with UITextField is detecting when the user presses the Return key. In many cases, you will want to automatically process the data and dismiss the keyboard when the user presses Return. To do so, you need to implement a UITextFieldDelegate method, as shown here.

ViewController.m (AutoReturn)

- (BOOL)textField:(UITextField *)textField

        shouldChangeCharactersInRange:(NSRange)range

        replacementString:(NSString *)string {

  if ([string isEqualToString:@” ”]) {

    self.outputLabel.text = [textField text];

    [textField resignFirstResponder];

    return NO;

  }

  return YES;

}

Whenever the user presses the Return key, a newline character ( ) is sent to this method. When that happens, you apply whatever processing you want (in this case, setting the text of another label) and then call resignFirstResponder to dismiss the keyboard. You can use a similar technique with UITextView.

UITextView is easily confused with UITextField, but it serves a somewhat different function. It’s intended for multiline, scrolling text, editing, or viewing. It’s a type of UIScrollView, not a UIControl. You can apply a font to the text, but the same font is used for the entire view. UITextView cannot display rich text from an NSAttributedString. There is no Apple-provided view that can do that. Generally, the choice between UITextField and UITextView is obvious based on how much text the user is going to type.

Rich Text in UIKit

One of the best new features in iOS 6 is the addition of rich text to UIKit. You can now display all kinds of useful information that used to require complex custom views or web views. Although there are still some reasons to use web views for rich text (see the later section “Web Views for Rich Text”), whenever possible use attributed strings. Before I discuss attributed strings, you need to understand the basics of the three most common rich text attributes: bold, italic, and underline.

Understanding Bold, Italic, and Underline

Bold, italic, and underline are generally presented to the user as simple attributes, but they’re quite different from each other. In typography, you do not “bold” a font by drawing it with thicker lines. Instead, the font designer provides a heavier weight version of the font, called a variation. In iOS the Helvetica font has a variation called Helvetica-Bold. Although these fonts are related, they’re completely different UIFont objects.

Italic is similar, but there are two related typefaces that are commonly treated as “italic.” True italic type is based on calligraphy and uses different shapes (glyphs) than the regular (or roman) font. Some fonts do not have a true italic variation and merely slant the roman type to the right. This is called oblique. When users request italic, they generally mean either italic or oblique. Text with both bold and italic requires yet another font variation, such as Helvetica-BoldOblique.

Unlike bold and italic, underline is a decoration like color or shadow. You do not change font when you add decorations. See Figure 26-1 for examples of bold, italic, and underline. Note carefully the significant difference between the glyphs of the roman font and the italic variation, whereas the underlined glyphs are identical to the roman font. Also note that underline is not as simple as drawing a line under the text. Proper underline includes breaks for descenders like p. All of these small details greatly improve the appearance and legibility of text in iOS.

9781118449974-fg2601.tif

Figure 26-1 Variations and decorations of Baskerville

Understanding these subtleties will help you understand why NSAttributedString works the way it does. For instance, there is no “bold” attribute as you might expect. You bold text by changing its font.

Attributed Strings

The fundamental data type for rich text is NSAttributedString. An attributed string is a string that applies attributes to ranges of characters. The attributes can be any key-value pair, but for the purposes of rich text, they usually contain style information such as font, color, and indentation.

It’s usually better to use NSMutableAttributedString so you can modify the attributes of various parts of the string. NSAttributedString requires that all of the strings have the same attributes.

In the following example, you create a basic rectangular layout to display some rich text in a UITextView. This project is available in the sample code named RichText. First, you add a UITextView in Interface Builder and select Attributed as its mode. Then you create an NSAttributedString and apply attributes to it. In this example, you create a string and apply a font attribute to all of it.

ViewController.m (RichText)

const CGFloat fontSize = 16.0;

  

// Create the base string.

// Note how you can define a string over multiple lines.

NSString *string =

@”Here is some simple text that includes bold and italics. ”

@” ”

@”We can even include some color.”;

  

// Create the mutable attributed string

NSMutableAttributedString *attrString =

[[NSMutableAttributedString alloc] initWithString:string];

NSUInteger length = [string length];

// Set the base font

UIFont *baseFont = [UIFont systemFontOfSize:fontSize];

[attrString addAttribute:NSFontAttributeName value:baseFont

                   range:NSMakeRange(0, length)];

As I discussed in the section “Understanding Bold, Italics, and Underline,” to apply bold, you need to apply a different font. You can do this simply for the system font, because you can easily request the bold system font:

// Apply bold using the bold system font

// and seaching for the word “bold”

UIFont *boldFont = [UIFont boldSystemFontOfSize:fontSize];

[attrString addAttribute:NSFontAttributeName value:boldFont

                   range:[string rangeOfString:@”bold”]];

But what if you’re using a non-system font and need the bold or italic variation? Unfortunately, UIKit doesn’t have a good way to determine variations of fonts, but Core Text does. After linking CoreText.framework, you can use the following function. It uses the name of the UIFont to find the correct CTFont. It then uses CTFontCreateCopyWithSymbolicTraits to add the requested trait (such as kCTFontTraitItalic). Finally, it uses the name of the resulting font to create the correct UIFont. Hopefully, this will become easier in future versions of iOS.

UIFont * GetVariationOfFontWithTrait(UIFont *baseFont,

                                     CTFontSymbolicTraits trait) {

  CGFloat fontSize = [baseFont pointSize];

  

  CFStringRef

  baseFontName = (__bridge CFStringRef)[baseFont fontName];

  CTFontRef baseCTFont = CTFontCreateWithName(baseFontName,

                                              fontSize, NULL);

  

  CTFontRef ctFont =

  CTFontCreateCopyWithSymbolicTraits(baseCTFont, 0, NULL,

                                     trait, trait);

  

  NSString *variantFontName =

  CFBridgingRelease(CTFontCopyName(ctFont,

                                   kCTFontPostScriptNameKey));

  

  UIFont *variantFont = [UIFont fontWithName:variantFontName

                                       size:fontSize];

  CFRelease(ctFont);

  CFRelease(baseCTFont);

  

  return variantFont;

}

...

UIFont *italicFont = GetVariationOfFontWithTrait(baseFont,

                                                 kCTFontTraitItalic);

[attrString addAttribute:NSFontAttributeName value:italicFont

                   range:[string rangeOfString:@”italics”]];

Finally, you can add color by adding the appropriate attribute:

// Apply color

UIColor *color = [UIColor redColor];

[attrString addAttribute:NSForegroundColorAttributeName

                   value:color

                   range:[string rangeOfString:@”color”]];

Notice the use of addAttribute:value:range: rather than setAttributes:value:range:. This merges the new attributes with the existing attributes, which is more often what you want.

After you’ve set all the attributes, you can display this string by setting the attributedText property of UITextView:

self.textView.attributedText = attrString;

Most UIKit controls that have a text property now also have an attributedText property, making it generally easy to convert existing code.

It’s important to note that NSAttributedString is not a subclass of NSString. It contains an NSString. Similarly, NSMutableAttributedString contains an NSMutableString. Although attributed strings have some of the more common string methods (like length), you often will need to fetch the underlying string using the string or mutableString methods.

Paragraph Styles

Some styles apply to paragraphs rather than characters. These include alignment, line break, and spacing. Paragraph attributes are bundled into an NSParagraphStyle object. You will almost always create an NSMutableParagraphStyle so that you can modify it. In this example, you modify the text alignment:

// Right justify the first paragraph

NSMutableParagraphStyle *

style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];

style.alignment = NSTextAlignmentRight;

[attrString addAttribute:NSParagraphStyleAttributeName

                   value:style

                   range:NSMakeRange(0, 1)];

Notice first that you create a mutableCopy of the defaultParagraphStyle. This is a very common pattern. You may also make a mutable copy of an existing paragraph style. Next, notice that the range for this style is just the first character. A “paragraph” starts at the beginning of the document or after a newline and continues to the next newline or the end of the document. The paragraph style of the first character applies to the entire paragraph. There is no way to change paragraph style within a paragraph.

Attributed Strings and HTML

Attributed strings are an incredibly convenient format for many tasks. By completely separating content from style information, it’s easy to search and edit. The full power of Cocoa’s strings handling routines is available. But attributed strings have one major problem: They are extremely cumbersome to build programmatically. In the RichText example, it took dozens of lines of code to format just a couple of sentences. On the Mac, it’s possible to create an attributed string from HTML, but there’s no built-in way to do this in iOS.

The fact that Apple did not directly port the NSAttributedString(AppKitAdditions) category that handles HTML is actually a blessing. The Mac conversion routines are buggy, slow, generate very complicated HTML, and have bizarre side effects when you use them. When Apple finally provides a converter between HTML and NSAttributedString, we should all hope they don’t use the Mac code.

Luckily, there is a very good solution: DTCoreText (formerly called NSAttributedString-Additions-for-HTML). DTCoreText can convert between simple HTML and NSAttributedString. It can also draw attributed strings in many ways that UIKit can’t. For example, it can embed images. At this writing, DTCoreText is not compatible with the NSAttributedString attributes in iOS 6, but this is in development and hopefully will be in place shortly after iOS 6 becomes available. If your problem lends itself well to HTML, definitely consider this package. See the “Further Reading” section for more information.

Web Views for Rich Text

Although UITextView is highly useful for laying out simple rich text, and DTCoreText will allow you to display simple HTML, sometimes you need a really complicated layout that is difficult to solve with these tools. Regrettably, the best tool available in some of these cases is UIWebView. I say “regrettably” because UIWebView is filled with problems. It’s slow, buggy, complicated to use, and difficult to debug. Even so, it’s sometimes the best solution.

In this section, you will learn how to use UIWebView as a layout engine rather than as a web viewer.

Displaying and Accessing HTML in a Web View

The code to load HTML into a UIWebView is fairly straightforward:

NSString *html = @”This is <i>some</i> <b>rich</b> text”;

[self.webView loadHTMLString:html baseURL:nil];

webView doesn’t yet contain the HTML string. This is only a request to load the string at some point in the future. You need to wait for the delegate callback webViewDidFinishLoad:. At that point, you can read the data in the web view using JavaScript:

- (void)webViewDidFinishLoad:(UIWebView *)webView {

  NSString *

  body = [self.webView

          stringByEvaluatingJavaScriptFromString:

          @”document.body.innerHTML”];

  // Use body

}

The only way to access the data inside the web view is through JavaScript and stringByEvaluatingJavaScriptFromString:. I recommend isolating this JavaScript to a single object to provide a simpler interface. For instance, you could create a MyWebViewController object that owns the UIWebView and provides a body property to set and retrieve the contents.

Responding to User Interaction

It’s fairly common in a rich text viewer to support some kind of user interaction. When the user taps a button or link, you may want to run some Objective-C code. You can achieve this by creating a special URL scheme. Rather than http, pick a custom scheme identifier. For example, you might have the following HTML:

<p>Click <a href=’do-something:First’>here</a> to do something.</p>

In this example, the URL scheme is do-something, and the resource specifier is First. When this link is tapped, the entire URL is sent to the delegate, and you can act on it like this:

- (BOOL)webView:(UIWebView *)webView

shouldStartLoadWithRequest:(NSURLRequest *)request

navigationType:(UIWebViewNavigationType)navigationType {

  NSURL *URL = request.URL;

  if ([URL.scheme isEqualToString:@”do-something”]) {

    NSString *message = URL.resourceSpecifier;

    // Do something with message

    return NO;

  }

  return YES;

}

Returning YES from this delegate method allows UIWebView to load the request. You should return NO for your custom scheme. Otherwise, UIWebView will try to load it and pass an error to webView:didFailLoadWithError:.

URL schemes are case-insensitive. The result of request.URL.scheme will always be lowercase, even if you use mixed-case in the HTML. I recommend using a hyphen (-) to separate words in the scheme. You can also use a period (.) or plus (+). The rest of the URL is case-sensitive.

Drawing Web Views in Scroll and Table Views

UIWebView cannot be embedded in a UIScrollView or UITableView because the web view’s event handling will interfere with the scroll view. This makes it effectively unusable for table view cells. Because web views have significant performance issues, they’re not appropriate for table view cells in any case. You should generally draw table view cells using UIKit or Core Text.

Instead of directly embedding the web view, you can capture the web view as a UIImage and then draw that image in the scroll view or table view. To capture a web view as a UIImage, you need to call renderInContext: on its layer, as shown here:

UIGraphicsBeginImageContext(self.webView.bounds.size);

[webView.layer renderInContext:UIGraphicsGetCurrentContext()];

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

You need to break web views larger than 1024 x 1024 into smaller pieces and render them individually. Web views are a useful addition to your toolbox, but I recommend avoiding them when possible. It’s very difficult to get an iOS-native look and feel in WebKit. Communicating through JavaScript is cumbersome and error-prone. When things do go wrong, it can be very difficult to debug web view interactions. So generally use attributed strings or DTCoreText or gain full control and switch to Core Text.

Core Text

Core Text is the low-level text layout and font-handling engine in iOS. It’s extremely fast and powerful. With it you can handle complex layouts like multicolumn text and even curved text.

Core Text is a C-based API that uses Core Foundation naming and memory management. If you’re not familiar with Core Foundation patterns, see Chapter 27.

Core Text also uses attributed strings. In most cases, Core Text is quite forgiving about the kind of attributed string you pass. For instance, you can use the keys NSForegroundColorAttributedName or kCTForegroundColorAttributeName, and they will work the same. You can generally use a UIColor or a CGColor interchangeably, even though these are not toll-free bridged. Similarly, you can use UIFont or CTFont and get the same results. The SimpleLayout project in the example code for this chapter shows how to create a CFMutableAttributedString, but I won’t go into detail about that here, since it’s very similar to NSMutableAttributedString, and you can use the two interchangeably.

Simple Layout with CTFramesetter

After you have an attributed string, you generally lay out the text using CTFramesetter. A framesetter is responsible for creating frames of text. A CTFrame (frame) is an area enclosed by a CGPath containing one or more lines of text. Once you generate a frame, you draw it into a graphics context using CTFrameDraw. In the next example, you draw an attributed string into the current view using drawRect:.

First, you need to flip the view context. Core Text was originally designed on the Mac, and it performs all calculations in Mac coordinates. The origin is in the lower-left corner—lower-left origin (LLO)—and the y coordinates run from bottom to top as in a mathematical graph. CTFramesetter doesn’t work properly unless you invert the coordinate space, as shown in the following code.

CoreTextLabel.m (SimpleLayout)

- (id)initWithFrame:(CGRect)frame {

  if ((self = [super initWithFrame:frame])) {

    CGAffineTransform

    transform = CGAffineTransformMakeScale(1, -1);

    CGAffineTransformTranslate(transform,

                               0, -self.bounds.size.height);

    self.transform = transform;

    self.backgroundColor = [UIColor whiteColor];

  }

  return self;

}

Before drawing the text, you need to set the text transform, or matrix. The text matrix is not part of the graphics state and is not always initialized the way you expect. It isn’t included in the state saved by CGContextSaveGState. If you’re going to draw text, always call CGContextSetTextMatrix in drawRect:.

- (void)drawRect:(CGRect)rect {

  CGContextRef context = UIGraphicsGetCurrentContext();

  CGContextSetTextMatrix(context, CGAffineTransformIdentity);

  // Create a path to fill. In this case, use the whole view

CGPathRef path = CGPathCreateWithRect(self.bounds, NULL);

  CFAttributedStringRef

  attrString = (__bridge CFTypeRef)self.attributedString;

  // Create the framesetter using the attributed string

  CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);

  // Create a single frame using the entire string (CFRange(0,0))

  // that fits inside of path.

  CTFrameRef

  frame = CTFramesetterCreateFrame(framesetter,

                                   CFRangeMake(0, 0),

                                   path,

                                   NULL);

  

  // Draw the frame into the current context

  CTFrameDraw(frame, context);

  CFRelease(frame);

  CFRelease(framesetter);

  CGPathRelease(path);

}

There’s no guarantee that all of the text will fit within the frame. CTFramesetterCreateFrame simply lays out text within the path until it runs out of space or runs out of text.

Creating Frames for Noncontiguous Paths

Since at least iOS 4.2, CTFramesetterCreateFrame has accepted nonrectangular and noncontiguous frames. The Core Text Programming Guide has not been updated since before the release of iPhoneOS 3.2 and is occasionally ambiguous on this point. Because CTFramePathFillRule was added in iOS 4.2, Core Text has explicitly supported complex paths that cross themselves, including paths with embedded holes.

CTFramesetter always typesets the text from top to bottom (or right to left for vertical layouts such as for Japanese). This works well for contiguous paths, but can be a problem for noncontiguous paths such as for multicolumn text. For example, you can define a series of columns this way.

ColumnView.m (Columns)

- (CGRect *)copyColumnRects {

  CGRect bounds = CGRectInset([self bounds], 20.0, 20.0);

  

  int column;

  CGRect* columnRects = (CGRect*)calloc(kColumnCount,

                                        sizeof(*columnRects));

  

  // Start by setting the first column to cover the entire view.

  columnRects[0] = bounds;

  // Divide the columns equally across the frame’s width.

  CGFloat columnWidth = CGRectGetWidth(bounds) / kColumnCount;

  for (column = 0; column < kColumnCount - 1; column++) {

    CGRectDivide(columnRects[column], &columnRects[column],

            &columnRects[column + 1], columnWidth, CGRectMinXEdge);

  }

  

  // Inset all columns by a few pixels of margin.

  for (column = 0; column < kColumnCount; column++) {

    columnRects[column] = CGRectInset(columnRects[column],

                                      10.0, 10.0);

  }

  return columnRects;

}

You have two choices for how to combine these rectangles. First, you can create a single path that contains all of them, like this:

CGRect *columnRects = [self copyColumnRects];

// Create a single path that contains all columns

CGMutablePathRef path = CGPathCreateMutable();

for (int column = 0; column < kColumnCount; column++) {

  CGPathAddRect(path, NULL, columnRects[column]);

}

free(columnRects);

This typesets the text as shown in Figure 26-2.

9781118449974-fg2602.tif

Figure 26-2 Column layout using a single path

Most of the time what you see in Figure 26-2 isn’t what you want. Instead, you need to typeset the first column, then the second column, and finally the third. To do so, you need to create three paths and add them to a CFMutableArray called paths:

CGRect *columnRects = [self copyColumnRects];

// Create an array of layout paths, one for each column.

for (int column = 0; column < kColumnCount; column++) {

  CGPathRef path = CGPathCreateWithRect(columnRects[column], NULL);

  CFArrayAppendValue(paths, path);

  CGPathRelease(path);

}

free(columnRects);

You then iterate over this array, typesetting the text that hasn’t been drawn yet:

CFIndex pathCount = CFArrayGetCount(paths);

CFIndex charIndex = 0;

for (CFIndex pathIndex = 0; pathIndex < pathCount; ++pathIndex) {

  CGPathRef path = CFArrayGetValueAtIndex(paths, pathIndex);

  

  CTFrameRef

  frame = CTFramesetterCreateFrame(framesetter,

                                   CFRangeMake(charIndex, 0),

                                   path,

                                   NULL);

  CTFrameDraw(frame, context);

  CFRange frameRange = CTFrameGetVisibleStringRange(frame);

  charIndex += frameRange.length;      

  CFRelease(frame);

}

The call to CTFrameGetVisibleStringRange returns the range of characters within the attributed string that are included in this frame. That lets you know where to start the next frame. The zero-length range passed to CTFramesetterCreateFrame indicates that the framesetter should typeset as much of the attributed string as will fit.

Using these techniques, you can typeset text into any shape you can draw with CGPath, as long as the text fits into lines. You find out how to handle more complicated cases in the section “Drawing Text Along a Curve” later in this chapter.

Typesetters, Lines, Runs, and Glyphs

The framesetter is responsible for combining typeset lines into frames that can be drawn. The typesetter is responsible for choosing and positioning the glyphs in those lines. CTFramesetter automates this process, so you usually don’t need to deal with the underlying typesetter (CTTypesetter). You will generally use the framesetter or move farther down the stack to lines, runs, and glyphs.

Starting at the bottom of the stack, a glyph (CGGlyph) is a shape that represents some piece of language information. This includes letters, numbers, and punctuation. It also includes whitespace, ligatures, and other marks. A ligature is when letters or other fundamental language units (graphemes) are combined to form a single glyph. The most common one in English is the fi ligature, formed when the letter f is followed by the letter i. In many fonts, these are combined into a single glyph to improve readability. The important thing is that a string may have a different number of glyphs than characters; the number of glyphs depends on the font and the layout of the characters.

A font can be thought of as a collection of glyphs, along with some metadata, such as the size and name. The CGGlyph type is implemented as an index into a CGFont. Don’t confuse this with CTFont or UIFont. Each drawing system has its own font type. Core Text also has a CTGlyphInfo type for controlling how Unicode characters are mapped to glyphs. This is rarely used.

The typesetter is responsible for choosing the glyphs for a given attributed string and for collecting them into runs. A run (CTRun) is a series of glyphs that has the same attributes and direction (such as left-to-right or right-to-left). Attributes include font, color, shadow, and paragraph style. You cannot directly create CTRun objects, but you can draw them into a context with CTRunDraw. Each glyph is positioned in the run, taking into account individual glyph size and kerning. Kerning is small adjustments to the spacing between glyphs to make text more readable. For example, the letters V and A are often kerned very close together.

The typesetter combines runs into lines. A line (CTLine) is a series of runs oriented either horizontally or vertically (for languages such as Japanese). CTLine is the lowest-level typesetting object that you can directly create from an attributed string. This is convenient for drawing small blocks of rich text. You can directly draw a line into a context using CTLineDraw.

Generally in Core Text, you work with either a CTFramesetter for large blocks or a CTLine for small labels. From any level in the hierarchy, you can fetch the lower-level objects. For example, given a CTFramesetter, you create a CTFrame, and from that you can fetch its array of CTLine objects. Each line includes an array of CTRun objects, and within each run is a series of glyphs, along with positioning information and attributes. Behind the scenes is the CTTypesetter doing most of the work, but you seldom interact with it directly.

In the next section, “Drawing Text Along a Curve,” you put all of these pieces together to perform a complex text layout.

Drawing Text Along a Curve

In this example, you use all the major parts of Core Text. Apple provides a somewhat simple example called CoreTextArcCocoa that demonstrates how to draw text along a semicircular arc. The Apple sample code is not very flexible, however, and is difficult to use for shapes other than a perfect circle centered in the view. It also forces the text to be evenly spaced along the curve. In this example, you learn how to draw text on any Bézier curve, and the techniques are applicable to drawing on any path. You also preserve Core Text’s kerning and ligatures. The end result is shown in Figure 26-3. This example is available from the downloads for this chapter, in CurvyTextView.m in the CurvyText project.

9781118449974-fg2603.tif

Figure 26-3 Output of CurvyTextView

Although CGPath can represent a Bézier curve and Core Graphics can draw it, there are no functions in iOS that allow you to calculate the points along the curve. You need these points, provided by Bezier(), and the slope along the curve, provided by BezierPrime():

CurvyTextView.m (CurvyText)

static double Bezier(double t, double P0, double P1, double P2,

                     double P3) {

  return

           (1-t)*(1-t)*(1-t)         * P0

     + 3 *       (1-t)*(1-t) *     t * P1

     + 3 *             (1-t) *   t*t * P2

     +                         t*t*t * P3;

}

static double BezierPrime(double t, double P0, double P1,

                          double P2, double P3) {

  return

    -  3 * (1-t)*(1-t) * P0

    + (3 * (1-t)*(1-t) * P1) - (6 * t * (1-t) * P1)

    - (3 *         t*t * P2) + (6 * t * (1-t) * P2)

    +  3 * t*t * P3;

}

P0 is the starting point, drawn in green by CurvyTextView. P1 and P2 are the control points, drawn in black. P3 is the endpoint, drawn in red. You call these functions twice, once for the x coordinate and once for y coordinate. To get a point and angle along the curve, you pass a number between 0 and 1 to pointForOffset: and angleForOffset:.

- (CGPoint)pointForOffset:(double)t {

  double x = Bezier(t, _P0.x, _P1.x, _P2.x, _P3.x);

  double y = Bezier(t, _P0.y, _P1.y, _P2.y, _P3.y);

  return CGPointMake(x, y);

}

- (double)angleForOffset:(double)t {  

  double dx = BezierPrime(t, _P0.x, _P1.x, _P2.x, _P3.x);

  double dy = BezierPrime(t, _P0.y, _P1.y, _P2.y, _P3.y);  

  return atan2(dy, dx);

}

These methods are called so many times that I’ve made an exception to the rule to always use accessors. This is the major hotspot of this program, and optimizations to speed it up or call it less frequently are worthwhile. There are many ways to improve the performance of this code, but this is fast enough for an example. See robnapier.net/bezier for more details on Bézier calculations.

With these two functions to define your path, you can now lay out the text. The following is a method to draw an attributed string into the current context along this path.

- (void)drawText {

  if ([self.attributedString length] == 0) { return; }

  

  // Initialize the text matrix (transform). This isn’t reset

  // automatically, so it might be in any state.  

  CGContextRef context = UIGraphicsGetCurrentContext();

  CGContextSetTextMatrix(context, CGAffineTransformIdentity);

  // Create a typeset line object

  CTLineRef line = CTLineCreateWithAttributedString(

                        (__bridge CFTypeRef)self.attributedString);

  

  // The offset is where you are in the curve, from [0, 1]

  double offset = 0.;

  

  // Fetch the runs and process one at a time

  CFArrayRef runs = CTLineGetGlyphRuns(line);

  CFIndex runCount = CFArrayGetCount(runs);

  for (CFIndex runIndex = 0; runIndex < runCount; ++runIndex) {

    CTRunRef run = CFArrayGetValueAtIndex(runs, runIndex);

    // Apply the attributes from the run to the current context

    [self prepareContext:context forRun:run];

    

    // Fetch the glyphs as a CGGlyph* array

    NSMutableData *glyphsData = [self glyphDataForRun:run];

    CGGlyph *glyphs = [glyphsData mutableBytes];

    // Fetch the advances as a CGSize* array. An advance is the

    // distance from one glyph to another.

    NSMutableData *advancesData = [self advanceDataForRun:run];

    CGSize *advances = [advancesData mutableBytes];

    

    // Loop through the glyphs and display them

    CFIndex glyphCount = CTRunGetGlyphCount(run);

    for (CFIndex glyphIndex = 0;

         glyphIndex < glyphCount && offset < 1.0;

         ++glyphIndex) {

      // You’re going to modify the transform, so save the state

      CGContextSaveGState(context);

      // Calculate the location and angle. This could be any

      // function, but here you use a Bezier curve

      CGPoint glyphPoint = [self pointForOffset:offset];      

      double angle = [self angleForOffset:offset];

      

      // Rotate the context

      CGContextRotateCTM(context, angle);

      // Translate the context after accounting for rotation

      CGPoint

      translatedPoint = CGPointApplyAffineTransform(glyphPoint,

                            CGAffineTransformMakeRotation(-angle));

      CGContextTranslateCTM(context,

                            translatedPoint.x, translatedPoint.y);      

      // Draw the glyph

      CGContextShowGlyphsAtPoint(context, 0, 0,

                                 &glyphs[glyphIndex], 1);

      

      // Move along the curve in proportion to the advance.

      offset = [self offsetAtDistance:advances[glyphIndex].width

                            fromPoint:glyphPoint offset:offset];

      CGContextRestoreGState(context);

    }

  }

}

The translation at the end of drawText is particularly important. All the glyphs are drawn at the origin. You use a transform to move the glyph into the correct position. The transform includes a rotation and a translation, and the order matters. If you rotate the context halfway around, and then translate the context up one point, the net effect will be to translate down one point. To account for this, you need to apply the inverse transform to the translation using CGPointApplyAffineTransform. The inverse of rotating by angle radians is to rotate by –angle radians. You could also use CGAffineTransformInvert to get the same effect.

You need a few more methods. First, you need to apply the attributes to the context. In this example, you just set the font and color, but you could support any of the attributes listed in CTStringAttributes.h, or add your own. Note that this supports only a CTFont, not a UIFont. They’re not interchangeable. If you want to support both the way CTFramesetter does, then you need to code for that. Similarly, this code supports only a CGColor, not a UIColor.

- (void)prepareContext:(CGContextRef)context forRun:(CTRunRef)run {

  CFDictionaryRef attributes = CTRunGetAttributes(run);

  // Set font

  CTFontRef runFont = CFDictionaryGetValue(attributes,

                                           kCTFontAttributeName);

  CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL);

  CGContextSetFont(context, cgFont);

  CGContextSetFontSize(context, CTFontGetSize(runFont));

  CFRelease(cgFont);

  

  // Set color

  CGColorRef color = (CGColorRef)CFDictionaryGetValue(attributes,

                                  kCTForegroundColorAttributeName);

  CGContextSetFillColorWithColor(context, color);

}

Fetching the glyph and advance information is easy. You just allocate a buffer and pass it to CTRunGetGlyphs or CTRunGetAdvances. The problem with these routines is that they copy all the data, which can be slow. There are faster versions (CTRunGetGlyphsPtr and CTRunGetAdvancesPtr) that return a pointer without making a copy. These versions can fail, however, if the data hasn’t been calculated yet. glyphDataForRun:, shown in the following code, handles both cases and returns an NSMutableData that automatically handles memory management. advanceDataForRun: is nearly identical. You can find the source for it in CurvyTextView.m.

- (NSMutableData *)glyphDataForRun:(CTRunRef)run {

  NSMutableData *data;

  CFIndex glyphsCount = CTRunGetGlyphCount(run);

  const CGGlyph *glyphs = CTRunGetGlyphsPtr(run);

  size_t dataLength = glyphsCount * sizeof(*glyphs);

  if (glyphs) {

    data = [NSMutableData dataWithBytesNoCopy:(void*)glyphs

                                length:dataLength freeWhenDone:NO];

  }

  else {

    data = [NSMutableData dataWithLength:dataLength];

    CTRunGetGlyphs(run, CFRangeMake(0, 0), data.mutableBytes);

  }

  return data;

}

Finally, to maintain proper spacing, you need to find the point along the curve that is the same distance as the advance. This is not trivial for a Bézier curve. Offsets are not linear, and it’s almost certain that the offset 0.25 will not be a quarter of the way along the curve. A simple solution is to repeatedly increment the offset and calculate a new point on the curve until the distance to that point is at least equal to your advance. The larger the increment you choose, the more characters tend to spread out. The smaller the increment you choose, the longer it takes to calculate. My experience is that values between 1/1000 (0.001) and 1/10,000 (0.0001) work well. Although 1/1000 has visible errors when compared to 1/10,000, the speed improvement is generally worth it. You could try to optimize this with a binary search, but that can fail if the loop wraps back on itself or crosses itself. Here is a simple implementation of the search algorithm:

- (double)offsetAtDistance:(double)aDistance

                 fromPoint:(CGPoint)aPoint

                    offset:(double)anOffset {

  const double kStep = 0.001; // 0.0001 - 0.001 work well

  double newDistance = 0;

  double newOffset = anOffset + kStep;

  while (newDistance <= aDistance && newOffset < 1.0) {

    newOffset += kStep;

    newDistance = Distance(aPoint,

                           [self pointForOffset:newOffset]);

  }

  return newOffset;

For more information on finding lengths on a curve, search the Web for “arc length parameterization.”

With these tools, you can typeset rich text along any path you can calculate.

Summary

Apple provides a variety of powerful text layout tools, from UILabel to UIWebView to Core Text. In this chapter, you looked at the major options and how to choose among them. Most of all, you should have a good understanding of how to use Core Text to create beautiful text layout in even the most complex applications.

Further Reading

Apple Documentation

The following documents are available in the iOS Developer Library at developer.apple.com or through the Xcode Documentation and API Reference.

Core Text Programming Guide

Quartz 2D Programming Guide: “Text”

String Programming Guide: “Drawing Strings”

NSAttributedString UIKit Additions Reference

Text, Web, and Editing Programming Guide for iOS

WWDC Sessions

The following session videos are available at developer.apple.com.

WWDC 2011, “Session 511: Rich Text Editing in Safari on iOS”

WWDC 2012, “Session 222: Introduction to Attributed Strings for iOS”

WWDC 2012, “Session 226: Core Text and Fonts”

WWDC 2012, “Session 230: Advanced Attributed Strings for iOS”

Other Resources

Clegg, Jay. Jay’s Projects. “Warping Text to a Bézier curves.” Useful background on techniques for laying out text along a curve. The article is in C# and GDI+, but the math is useful on any platform.planetclegg.com/projects/WarpingTextToSplines.html

Drobnik, Oliver. DTCoreText. Powerful framework for drawing attributed strings in iOS and converting between attributed strings and HTML.github.com/Cocoanetics/DTCoreText

Kosmaczewski, Adrian. CoreTextWrapper. Wrapper to simplify multicolumn layout using Core Text.github.com/akosma/CoreTextWrapper

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.72.133