iOS Recipes - Matt Drance [21]
Recipe 10 Make a Label for Attributed Strings
Problem
The iOS label class cannot display an attributed string—that is, a string that contains “rich text” formats such as underline, colors, or mixed fonts.
Solution
When Apple added the Core Text APIs to iOS for lower-level text rendering, it also needed to include the NSAttributedString class, providing significantly more power for formatting text. Although OS X has the ability to render attributed strings through UI controls, iOS currently does not.
Core Text is a very deep API, dealing with glyphs and kerning, text runs, and lines, so it would be nice if you could solve this problem without having to dig too far.
Thankfully, Core Text does provide a very simple method that we can use to create a line of attributed text. We can then take that line and draw it into any graphics context (see Figure 14, TableView of attributed labels ). It makes sense to make our new class, PRPAttributedLabel, a UIView subclass because that provides the simplest way to get access to the graphics context we need.
Figure 14. TableView of attributed labels
* * *
The drawRect: method contains only three lines of code that deal directly with creating and rendering the attributed string. The majority of the code deals with fetching a reference to the context, saving and restoring the context state, and translating the context coordinates to match the inverted iOS coordinate system.
The first method, CTLineCreateWithAttributedString, creates a very simple Core Text line without the need for a typesetter object, because typesetting is done for us under the hood. We then set the position for the line within the frame of the view, using CGContextSetTextPosition. The current text position is in the center of the frame, so we need to calculate the offsets relative to that; in this simple case, we start at the left edge and move up one quarter of the frame’s height from the bottom. None of this positioning takes into account the attributes of the string, such as font size, so we will need to adjust our label frame size to fit the fonts used in our attributed strings. Just as with UILabel, it may take a little trial and error to get the lines to fit well.
Finally, we call the CTLineDraw method to draw the line into the graphics context at the specified point.
coreText/Classes/PRPAttributedLabel.m
- (void)drawRect:(CGRect)rect {
if (self.attributedText == nil)
return;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextTranslateCTM(context, self.bounds.size.width/2,
self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)
self.attributedText);
CGContextSetTextPosition(context, ceill(-self.bounds.size.width/2),
ceill(self.bounds.size.height/4));
CTLineDraw(line, context);
CGContextRestoreGState(context);
CFRelease(line);
}
We are usually happy to let the @synthesize directive build our property setters for us, but in this case we need to make sure that any changes to the attributed string trigger a redraw so that the label is updated for every change. To do that, we need to create a customized setter for the attributedString property, which will contain the additional setNeedsDisplay call to force the redraw.
coreText/Classes/PRPAttributedLabel.m
- (void)setAttributedText:(NSAttributedString *)newAttributedText {
if (attributedText != newAttributedText) {
[attributedText release];
attributedText = [newAttributedText copy];
[self setNeedsDisplay];
}
}
In the sample code we use a custom UITableViewController to show a list of available fonts. This is mostly boilerplate code, but in the tableView:cellForRowAtIndex: delegate