BuySellAds.com

My book Barcodes with iOS is nearing completion. Buy it now to get early access!
Our DNA is written in Objective-C
Jump

Befriending Core Text

Before the iPad was released you had basically two ways how to get text on screen. Either you would stick with UILabel or UITextView provided by UIKit or if you felt hard-core you would draw the text yourself on the Quartz level incurring all the headaches induced by having to mentally switch between Objective-C and C API functions.

As of iOS 3.2 we gained a third alternative in Core Text promising full control over styles, thread safety and performance. However for most of my apps I did not want to break 3.x compatibility and so I procrastinated looking at this powerful new API. Apps running only on iPads could have made use of Core Text from day 1, but to me it made more sense supporting iPad via hybrid apps where the iPhone part would still be backwards compatible.

Now as the year has turned the adoption of 4.x on all iOS platforms is ever more accelerating. Many new iPads where found under the Christmas tree and by now even the most stubborn people (read needing 3.x for jailbreaking and sim-unlocking) have little reason to stick with 3.x. Thus we have almost no incentive left to stick with 3.x compatibility. Yay!

Which brings us to Core Text. This API has been available on OSX for a while already, some of the advanced features like vertical text have been left out of the iPhone API.

Why do I need it?

Which methods do we know to draw text on screen? We have the low-level text drawing functions on the CoreGraphics level. We have UIKit classes UILabel and UITextView. But neither of these provide any easy way to draw mixed-style text. Often developers have to resort to using HTML in UIWebView just to draw something like bold words or a hyperlink amongst regular plain text.

This is the need that gets filled by CoreText. Almost.

The basic unit to describe strings with formatting are NSAttributedString and its mutable descendant NSMutibleAttributedString. As their names suggest they consist of NSStrings where different parts can have varying attributes. Now if you look at the documentation of NSAttributedString on OSX they have methods to create such strings straight from HTML. Boy that would be really handy. But until Apple chooses to port these methods over to iOS we are stuck with creating our own attributed strings.

Nevertheless even at this somewhat crippled stage CoreText is worth looking at to ease some of the mixed-style text drawing problems we might be facing.

Drawing “Simple” Lines – Not So Simple

At its simplest you draw individual lines with Core Text. For this you create a font, create an attributed text and then just draw the line. Even at its simplest there is quite a bit of typing involved. Of course we need to add the CoreText framework and header, in this case I am customizing a UIView subclass.

- (void)drawRect:(CGRect)rect
{
	// create a font, quasi systemFontWithSize:24.0
	CTFontRef sysUIFont = CTFontCreateUIFontForLanguage(kCTFontSystemFontType,
		24.0, NULL);
 
	// create a naked string
	NSString *string = @"Some Text";
 
	// blue
	CGColorRef color = [UIColor blueColor].CGColor;
 
	// single underline
	NSNumber *underline = [NSNumber numberWithInt:kCTUnderlineStyleSingle];
 
	// pack it into attributes dictionary
	NSDictionary *attributesDict = [NSDictionary dictionaryWithObjectsAndKeys:
		(id)sysUIFont, (id)kCTFontAttributeName,
		color, (id)kCTForegroundColorAttributeName,
		underline, (id)kCTUnderlineStyleAttributeName, nil];
 
	// make the attributed string
	NSAttributedString *stringToDraw = [[NSAttributedString alloc] initWithString:string
		attributes:attributesDict];
 
	// now for the actual drawing
	CGContextRef context = UIGraphicsGetCurrentContext();
 
	// flip the coordinate system
	CGContextSetTextMatrix(context, CGAffineTransformIdentity);
	CGContextTranslateCTM(context, 0, self.bounds.size.height);
	CGContextScaleCTM(context, 1.0, -1.0);
 
	// draw
	CTLineRef line = CTLineCreateWithAttributedString(
		(CFAttributedStringRef)stringToDraw);
	CGContextSetTextPosition(context, 10.0, 10.0);
	CTLineDraw(line, context);
 
	// clean up
	CFRelease(line);
	CFRelease(sysUIFont);
	[stringToDraw release];
}

It’s particularly unnerving that Apple is mixing CF-style and Objective-C , but we have to live with that.

Another Font Class

Note that the core text font object used here is not the one we are used to using, UIFont. Thus we also don’t get the toll-free bridging to CGFontRef. This one here is optimized for Core Text. The example uses the method analogous to systemFontWithSize:, there are several more options to get a usable font.

// create it from the postscript name
CTFontRef helveticaBold = CTFontCreateWithName(CFSTR("Helvetica-Bold"), 24.0, NULL);
 
// create it by replacing traits of existing font, this replaces bold with italic
CTFontRef helveticaItalic = CTFontCreateCopyWithSymbolicTraits(helveticaBold, 24.0, NULL,
			kCTFontItalicTrait, kCTFontBoldTrait | kCTFontItalicTrait);

Where a font name is used you have to use the postscript name. A sample list of all built-in fonts on iPhone/iPad refer to my article on Understanding UIFont. As of iOS 3.2 you can also add your own fonts by registering them in the info.plist and adding them to the app bundle.

To appreciate the level of customization available to use consider the different font traits available, on UIFont we had only normal, bold, italic and bold-italic.

  • Italic
  • Bold
  • Expanded
  • Condensed
  • MonoSpace
  • Vertical
  • Optimized for rendering UI Elements

If the above has not tied your brain in a knot yet, then thinking about the transformation matrices surely will. You are dealing with multiple coordinate systems which have their origin (0,0) in different locations. Quartz generally has it in the lower left, CoreText in the upper left. Add to this the fact that UIKit preflips the context you get to use in drawRect.

Underlining is not a feature of the font itself, but of NSAttributedString, likely because you can underline text in ANY font. These are your options:

  • None
  • Single Line
  • Double Line
  • Thick Line

These can be bitwise OR combined with several underlining line styles:

  • Solid Line
  • Dotted Line
  • Dashed Line
  • Dash-Dotted Line
  • Dash-Dot-Dotted Line

Transformers

For this graphic I drew the same text once without flipped transformation matrix and once with flipping it. Behind it have a rectangle (0,0,100,100). So you can see that without flipping the text is on it’s head. With flipping it’s right side up, but now located in relation to the lower left corner. Additional confusion is added by the fact that you don’t position the outer bounding rect of the text, but instead the baseline causing descenders and the underline to be outside of the rectangles.

I would have preferred to not having to do this flip-flopping. But hey, if it where too easy then everybody would be using CoreText. :-)

Layout

Using the Layout API instead of drawing individual lines gives you several powerful benefits. You have access to muliple styles, get fine control over text layout. You can even substitute text, like replacing instances of “Copyright” with the copyright symbol or making space to insert an image in the flow of the text.

The first concept to understand is that you have multiple objects which form a hierarchy in figuring out how the text would flow. This screenshot I got from the WWDC 2010 Session 110 on Advanced Text Handling for iOS. The Framesetter controls the frames in which text can flow, i.e. pages and/or columns. You’ll want to avoid recreating it often because there is a certain cost attached to doing so.

Currently only rectangles are supported to specify the bounds of such a frame. But conceivably Apple will allow us to use arbitrary paths in the future. I tried adding multiple CGRect to a path to see what would happen and it turns out that the text continues to flow horizontally between frames. Not what we would want if we have a 2 column layout in mind.

The Framesetter knows how much text fits into one frame and you can query the frame as to what range from the attributed string fit. Adding a second frame is analogous to the creating the first with the only difference that you have to specify the index to start from.

We already used a CTLine in the simple drawing example, one frame is made up of as many such lines as fit into the bounds of the frame. The smallest unit if drawing in CoreText is a Glyph Run, which are basically several characters with identical attributes. A special feature on iOS is that you can specify a run delegate which is able to customize the look of individual runs.

For an example lets create a long text and have this flow in two columns. As additional demonstration set bold font and text color for some spots.

Now brace yourself, there is quite a lot of code to achieve that, especially because there is no easier way to add the attributes.

- (void)drawRect:(CGRect)rect
{
	NSString *longText = @"Lorem ipsum dolor sit amet, "; /* ... */
 
	NSMutableAttributedString *string = [[NSMutableAttributedString alloc]
		initWithString:longText];
 
	// make a few words bold
	CTFontRef helvetica = CTFontCreateWithName(CFSTR("Helvetica"), 14.0, NULL);
	CTFontRef helveticaBold = CTFontCreateWithName(CFSTR("Helvetica-Bold"), 14.0, NULL);
 
	[string addAttribute:(id)kCTFontAttributeName
		value:(id)helvetica
		range:NSMakeRange(0, [string length])];
 
	[string addAttribute:(id)kCTFontAttributeName
		value:(id)helveticaBold
		range:NSMakeRange(6, 5)];
 
	[string addAttribute:(id)kCTFontAttributeName
		 value:(id)helveticaBold
		range:NSMakeRange(109, 9)];
 
	[string addAttribute:(id)kCTFontAttributeName
		value:(id)helveticaBold
		range:NSMakeRange(223, 6)];
 
	// add some color
	[string addAttribute:(id)kCTForegroundColorAttributeName
		 value:(id)[UIColor redColor].CGColor
		range:NSMakeRange(18, 3)];
 
	[string addAttribute:(id)kCTForegroundColorAttributeName
		value:(id)[UIColor greenColor].CGColor
		range:NSMakeRange(657, 6)];
 
	[string addAttribute:(id)kCTForegroundColorAttributeName
		value:(id)[UIColor blueColor].CGColor
		range:NSMakeRange(153, 6)];
 
	// layout master
	CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(
		(CFAttributedStringRef)string);
 
	// left column form
	CGMutablePathRef leftColumnPath = CGPathCreateMutable();
	CGPathAddRect(leftColumnPath, NULL, 
		CGRectMake(0, 0, 
			self.bounds.size.width/2.0,
			self.bounds.size.height));
 
	// left column frame
	CTFrameRef leftFrame = CTFramesetterCreateFrame(framesetter, 
		CFRangeMake(0, 0),
			leftColumnPath, NULL);
 
	// right column form
	CGMutablePathRef rightColumnPath = CGPathCreateMutable();
	CGPathAddRect(rightColumnPath, NULL, 
		CGRectMake(self.bounds.size.width/2.0, 0, 
			self.bounds.size.width/2.0,
			self.bounds.size.height));
 
	NSInteger rightColumStart = CTFrameGetVisibleStringRange(leftFrame).length;
 
	// right column frame
	CTFrameRef rightFrame = CTFramesetterCreateFrame(framesetter,
		CFRangeMake(rightColumStart, 0),
			rightColumnPath,
			NULL);
 
	// flip the coordinate system
	CGContextRef context = UIGraphicsGetCurrentContext();
	CGContextSetTextMatrix(context, CGAffineTransformIdentity);
	CGContextTranslateCTM(context, 0, self.bounds.size.height);
	CGContextScaleCTM(context, 1.0, -1.0);
 
	// draw
	CTFrameDraw(leftFrame, context);
	CTFrameDraw(rightFrame, context);
 
	// cleanup
	CFRelease(leftFrame);
	CGPathRelease(leftColumnPath);
	CFRelease(rightFrame);
	CGPathRelease(rightColumnPath);
	CFRelease(framesetter);
	CFRelease(helvetica);
	CFRelease(helveticaBold);
	[string release];
}

This really cries out for having some helper methods to simplify the process. Also in real life you probably would not want to recreate the framesetter on every drawRect. Instead you would hold it in an instance variable and release it at the end of the life of this view.

Bonus Section: Strikethrough Text

On OSX there is an attribute to specify that text should be in strikethrough style. Unfortunately not on iOS. Let me show you a method to work around this limitation.

Basically you can add any kind of custom attribute to NSAttributedString, I am adding a bool NSNumber to specify that I want an area to be strikethrough.

// add custom attribute
[string addAttribute:@"DTCustomStrikeOut"
			   value:[NSNumber numberWithBool:YES]
			   range:NSMakeRange(18, 138)];

After drawing our frames I am getting the lines from the framesetter. In there I am getting the glyph runs all the while tracking where we are in screen coordinates. This way we can calculate a box for each glypth run and then draw a horizontal line.

// reset text position
CGContextSetTextPosition(context, 0, 0);
 
// get lines
CFArrayRef leftLines = CTFrameGetLines(leftFrame);
CGPoint *origins = malloc(sizeof(CGPoint)*[(NSArray *)leftLines count]);
CTFrameGetLineOrigins(leftFrame,
	CFRangeMake(0, 0), origins);
NSInteger lineIndex = 0;
 
for (id oneLine in (NSArray *)leftLines)
{
	CFArrayRef runs = CTLineGetGlyphRuns((CTLineRef)oneLine);
	CGRect lineBounds = CTLineGetImageBounds((CTLineRef)oneLine, context);
 
	lineBounds.origin.x += origins[lineIndex].x;
	lineBounds.origin.y += origins[lineIndex].y;
	lineIndex++;
	CGFloat offset = 0;
 
	for (id oneRun in (NSArray *)runs)
	{
		CGFloat ascent = 0;
		CGFloat descent = 0;
 
		CGFloat width = CTRunGetTypographicBounds((CTRunRef) oneRun,
			CFRangeMake(0, 0),
			&ascent,
			&descent, NULL);
 
		NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes((CTRunRef) oneRun);
 
		BOOL strikeOut = [[attributes objectForKey:@"DTCustomStrikeOut"] boolValue];
 
		if (strikeOut)
		{
			CGRect bounds = CGRectMake(lineBounds.origin.x + offset,
				lineBounds.origin.y,
				width, ascent + descent);
 
			// don't draw too far to the right
			if (bounds.origin.x + bounds.size.width > CGRectGetMaxX(lineBounds))
			{
				bounds.size.width = CGRectGetMaxX(lineBounds) - bounds.origin.x;
			}
 
			// get text color or use black
			id color = [attributes objectForKey:(id)kCTForegroundColorAttributeName];
 
			if (color)
			{
				CGContextSetStrokeColorWithColor(context, (CGColorRef)color);
			}
			else
			{
				CGContextSetGrayStrokeColor(context, 0, 1.0);
			}
 
			CGFloat y = roundf(bounds.origin.y + bounds.size.height / 2.0);
			CGContextMoveToPoint(context, bounds.origin.x, y);
			CGContextAddLineToPoint(context, bounds.origin.x + bounds.size.width, y);
 
			CGContextStrokePath(context);
		}
 
		offset += width;
	}
}
 
// cleanup
free(origins);

Warning: I am not certain that the calculation of screen positions is perfect, I might have misunderstood something. So please tell me in the comments if I made a mistake. Also if there actually is an easier way. I searched for hours and could not find one.

Conclusion

CoreText on iOS is a step in the right direction and using it can be beneficial if you are not afraid of constantly switching between the NS and CF worlds. To be actually of real use to us there needs to be work done on creating some convenience methods to be able to create NSAttributedStrings way more easily.

I envision that next we’d like to create a simplified HTML parser that would generate the appropriate attributed strings for us so that we can at least keep the formatted text in files as opposed to having to hand-code each bold word individually. Next it would be nice to create replacements for UILabel and UITextView that take attributed strings. This would enable us to display rich text.

The holy grail of texting definitely is to have copy/paste and editing support for these attributed text views. In one word: Rich Text Editing. But that’s a long way off. Maybe the iWork apps are an omen and Apple is really using them as a test case for that. If this is the case then we might already see the missing pieces in iOS 5 coming Summer 2011.


Categories: Recipes

%d bloggers like this: