Core Text on iOS - Laying out text and images

I am currently working on a project for a client that is taking their print publications and putting them onto the ipad. This being my first foray into a newstand app i have been learning an insane amount about laying out text in iOS. They are quite a few shortcuts iOS provides for drawing text on screen but if you want to do anything remotely sophisticated you need Core Text.

Core Text is a low level library for handling fonts and text layout in OSX. It was dropped into iOS as of the 3.2 release. Core text is extremely efficient and is well integrated with all of the other 'Core' apple frameworks.

Before you start playing with Core Text remember to TAKE YOUR TIME and READ THE DOCUMENTATION. Not only do you need to actually READ the documentation you should also take a look at apple's sample code. Im always dumbfounded by how many problems i have tried to solve, only to find a solution in apple's sample codebase.

Wrapping text around images

In HTML wrapping text around images is a fairly straightforward procedure as long as you have done your homework on CSS floats. In iOS its fairly simple as well but using Core Text isnt as simple as the (CGSize)drawInRect:(CGRect)rect withFont:(UIFont *)font method so lets walk through a basic example. All the code is in github so you can download the repo and have your way with it. The code will run on the ipad and iphone but i have only added enough text in the Localizable strings file to fill up the iphone viewport.

Step 1 : Draw some text in a rectangle

The first step in the process is to actually draw some text. For this example we are just going to draw text in the entire view with some padding applied. All of the code below goes in your drawRect:(CGRect)rect method.

/* Define some defaults */
float padding = 10.0f;
CTFontRef font = CTFontCreateWithName((__bridge CFStringRef) [UIFont systemFontOfSize:12].fontName, [UIFont systemFontOfSize:12].lineHeight, NULL);

/* Get the graphics context for drawing */
CGContextRef ctx = UIGraphicsGetCurrentContext();

/* Core Text Coordinate System is OSX style */
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
CGContextTranslateCTM(ctx, 0, self.bounds.size.height);
CGContextScaleCTM(ctx, 1.0, -1.0);

CGRect textRect = CGRectMake(padding, padding, self.frame.size.width - padding*2, self.frame.size.height - padding*2);

/* Create a path to draw in and add our text path */
CGMutablePathRef pathToRenderIn = CGPathCreateMutable();
CGPathAddRect(pathToRenderIn, NULL, textRect);

/* Build up an attributed string with the correct font */
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"TEXT", @"")];
CFAttributedStringSetAttribute((__bridge CFMutableAttributedStringRef) attrString, CFRangeMake(0, attrString.length), kCTFontAttributeName, font);

/* Get a framesetter to draw the actual text */
CTFramesetterRef fs = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) attrString);
CTFrameRef frame = CTFramesetterCreateFrame(fs, CFRangeMake(0, attrString.length), pathToRenderIn, NULL);

/* Draw the text */
CTFrameDraw(frame, ctx);

/* Release the stuff we used */
CFRelease(frame);
CFRelease(pathToRenderIn);
CFRelease(fs);

Ok so that wasn't too bad. Just to recap real quick all we are doing here is defining the entire view bounds as our rectangle to draw in. We are then creating an attributed string with a string from our localization file then drawing that text into a rectangle. We also then need to release anything that we used from Core Text in the process.

2 IMPORTANT THINGS TO NOTE The first is that the coordinate system for Core Text is the OSX system which is flipped from iOS! We will see later this affects how we defined our image clipping rectangle but for now we dont really notice it. Also note the __bridge stuff going on. I havent read up enough on this but my sense is that its basically allowing your ARC objects to work in the lower level C depths.

Ok so lets move on to defining where an image should go and clipping that text to draw around it.

Step 2 : Define where you want the image

Remember how i just said that the coordinate system is different. This tripped me up big time. Its one of the reasons why you need to actually read the Core Text proramming guide before you start messing around with it. With that in mind lets define where we want to draw our image and then add that image to the drawing path.

/* Add a image path to clip around */
CGRect clipRect = CGRectMake(padding, padding, 100, 100);
CGPathAddRect(pathToRenderIn, NULL, clipRect);

Notice here that we are drawing at origin (padding, padding) or (10,10) which in iOS would be in the top left corner. Since Core Text is OSX style it draws in the bottom left! This is also the reason why we need the following code snippet at the top of our drawRect:(CGRect)rect method.

/* Core Text Coordinate System is OSX style */
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
CGContextTranslateCTM(ctx, 0, self.bounds.size.height);
CGContextScaleCTM(ctx, 1.0, -1.0);

If you are feeling adventurous comment out those lines. You will notice that your text is drawn upside down.

You now have just enough knowledge to be dangerous with CoreText :)