CoreText example: Column Layout (correctly inverted text)

You can find an “Columnar layout” example at apple.com library, but as for today it’s targeted to Mac OSX, not iPhone or iPad development, and causes lots of confusion and even more errors.

I’ve changed it a bit and after some trial and error managed to create working CoreText example, which puts text in two columns. Oh, and letters are proper side up – looks like CoreText and iPhone works in different coordinate system. Most important lines to fix this issue are:

CGContextTranslateCTM(context, 0, _columnHeight);
CGContextScaleCTM(context, 1.0, -1.0);

CGContextTranslateCTM – Changes the origin of the user coordinate system in a context. It’s important to set your columns and CGContextTranslateCTM argument to the same height. I’ve spent too much time trying to figure out why everything is messed up while my columns was much shorter then the height I was passing to this function.
CGContextScaleCTM – Changes the scale of the user coordinate system in a context.

Result before these coordinate changes:

Before that massive chunk of code, I just want to be clear, that I’ve declared some variables in .h file, so use your own:

_columnCount = 2;
_columnHeight = 300;

And full example bellow:

- (CFArrayRef)createColumns {
    CGRect bounds = CGRectMake(0, 0, 400, _columnHeight);
    int column;

    CGRect* columnRects = (CGRect*)calloc(_columnCount, 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) / _columnCount;

    for (column = 0; column < _columnCount - 1; column++) {
        CGRectDivide(columnRects[column], &columnRects[column],&columnRects[column + 1], columnWidth, CGRectMinXEdge);
    }

	//	add margin
    for (column = 0; column < _columnCount; column++) {
        columnRects[column] = CGRectInset(columnRects[column], 10.0, 10.0);

    }

	// Create an array of layout paths, one for each column.
    CFMutableArrayRef array = CFArrayCreateMutable(kCFAllocatorDefault, _columnCount, &kCFTypeArrayCallBacks);
    for (column = 0; column < _columnCount; column++) {
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, columnRects[column]);
        CFArrayInsertValueAtIndex(array, column, path);
        CFRelease(path);
    }

    free(columnRects);
    return array;
}

- (void)drawRect:(CGRect)rect {
    [[UIColor whiteColor] set];
	CGContextRef context = (CGContextRef)UIGraphicsGetCurrentContext();

    CGContextTranslateCTM(context, 0, _columnHeight);
    CGContextScaleCTM(context, 1.0, -1.0);

    [UIBezierPath bezierPathWithRect:[self bounds]];

	CFStringRef string = (CFStringRef) @"Long text.\n\nfoobarpig.com";
	CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(CFAttributedStringCreate(NULL, string, NULL));

    CFArrayRef columnPaths = [self createColumns];
    CFIndex pathCount = CFArrayGetCount(columnPaths);

    CFIndex startIndex = 0;

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

        CGPathRef path = (CGPathRef)CFArrayGetValueAtIndex(columnPaths, column);

        // Create a frame for this column and draw it.
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(startIndex, 0), path, NULL);
        CTFrameDraw(frame, context);

        // Start the next frame at the first character not visible in this frame.
        CFRange frameRange = CTFrameGetVisibleStringRange(frame);
        startIndex += frameRange.length;
        CFRelease(frame);
    }
    CFRelease(columnPaths);
}

Final result:

Last thing: do not forget to add CoreText framework to your project and #import <CoreText/CoreText.h>. That’s all I guess.

Tags: , , , ,

7 responses

  1. Parvez Qureshi
    Posted July 27, 2010 at 04:20 | Permalink

    Very nice thing!!
    For a situation where one needs to render a mix of text and images then we are left with a choice of using an array of NSMutableAttributedString and UIImage(s). The only thing of concern here is that for a multiline text I am not able to get the exact height so that one can place the next frame after the previous one

    • Foobar Pig
      Posted July 28, 2010 at 19:00 | Permalink

      If I understand you correctly, you don’t need to have an array of NSMutableAttributedString. Just edit an above example to create an array of frames which will be placed around UIImage(s). It will be quite easy except cleaning that “inverted coordinate system” mess.

      Well, at least that’s what I did and as soon as I have some spare time I will post my code here.

  2. Parvez Qureshi
    Posted July 29, 2010 at 05:39 | Permalink

    In order to calculate the height of multiline frame I am using the below given code:

    //**********************************CODE*******************************
    
    -(CGFloat)measureFrame: (CTFrameRef)frame{
    	CGPathRef framePath = CTFrameGetPath(frame);
    	CGRect frameRect = CGPathGetBoundingBox(framePath);
    
    	CFArrayRef lines = CTFrameGetLines(frame);
    	CFIndex numLines = CFArrayGetCount(lines);
    
    	CGFloat maxWidth = 0;
    	CGFloat textHeight = 0;
    
    	// Now run through each line determining the maximum width of all the lines.
    	// We special case the last line of text. While we've got it's descent handy,
    	// we'll use it to calculate the typographic height of the text as well.
    	CFIndex lastLineIndex = numLines - 1;
    	for(CFIndex index = 0; index  maxWidth)
    		{
    			maxWidth = width;
    		}
    
    		if(index == lastLineIndex)
    		{
    			// Get the origin of the last line. We add the descent to this
    			// (below) to get the bottom edge of the last line of text.
    			CGPoint lastLineOrigin;
    			CTFrameGetLineOrigins(frame, CFRangeMake(lastLineIndex, 1), &amp;lastLineOrigin);
    
    			// The height needed to draw the text is from the bottom of the last line
    			// to the top of the frame.
    			textHeight =  CGRectGetMaxY(frameRect) - lastLineOrigin.y + descent;
    		}
    	}
    
    	// For some text the exact typographic bounds is a fraction of a point too
    	// small to fit the text when it is put into a context. We go ahead and round
    	// the returned drawing area up to the nearest point.  This takes care of the
    	// discrepencies.
    	//return CGSizeMake(ceil(maxWidth), ceil(textHeight));
    	return ceil(textHeight);
    
    //********************************CODE**********************************
    

    Whatever height i gets from this method will be added to both the ongoing CGRect in order to calculate the space available for next stuff to be rendered. The problem here is that height of available space is decreasing from 460 [for iphone] while y is increasing from 0. At a certain point where height value and y coordinate value are near to each other which gives incorrect results in this line

    textHeight = CGRectGetMaxY(frameRect) – lastLineOrigin.y + descent;
    for a multiline text frame
    Can you suggest any alternative way to use instead

  3. Parvez Qureshi
    Posted July 29, 2010 at 09:41 | Permalink

    Thanks for your reply. I have already tried CTFramesetterSuggest…. function earlier but also givena second try [with your suggested link]. It is returning much bigger frame then it must be. Does the use of multiple fonts is the reason behind this because it may be that a line may consists of multiple fonts [though all from Helvetica] with bold/oblique attributes.
    When you use CTLineGetTypographicBounds method will it not consider multiple fonts being used in that line?
    Modified my above function to be as below

    //*******************************CODE*********************************

    -(CGFloat)measureFrame: (CTFrameRef)frame{
    CFArrayRef lines = CTFrameGetLines(frame);
    NSUInteger n = CFArrayGetCount(lines);
    CTLineRef firstLine;
    CGFloat height = 0.0;
    CGFloat asscent, descent, leading;
    if (n == 1) {
    firstLine = (CTLineRef)[(NSArray*)lines objectAtIndex:0];
    CTLineGetTypographicBounds(firstLine, &asscent, &descent, &leading);
    height = asscent + descent + leading;
    return height;
    }
    CFIndex numLines = CFArrayGetCount(lines);
    CFIndex lastLineIndex = numLines – 1;
    for( CFIndex index = 0; index < numLines; index++){
    CTLineRef line = (CTLineRef) CFArrayGetValueAtIndex(lines, index);
    CTLineGetTypographicBounds(line, &asscent, &descent, &leading);
    if (index != lastLineIndex) {
    height += asscent + leading;
    }else {
    height += leading;
    }
    }

    return height;
    }

    //******************************CODE**********************************

    Though this function appears to return a correct height.[not sure if this is true]. Please must give your comments on this

  4. Parvez Qureshi
    Posted July 29, 2010 at 10:26 | Permalink

    Though using the above method seems to return correct frame height but now CoreText is not stopping drawing where it must stop [at the bottom] and continues drawing past the bottom of screen. Below given is my entire drawRect code

    //*********************************************CODE*****************************

    - (void)drawRect:(CGRect)rect{
    // Initialize a graphics context and set the text matrix to a known value.
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGRect textBounds = rect;
    textBounds.origin.y = -10;
    textBounds.size.width-= 20;
    textBounds.size.height-= 10;

    CGContextTranslateCTM(context, 0, textBounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    NSArray* arrayOfElements = [_delegate getArrayOfDataToRender];
    int arrayIndex = 0;
    for (arrayIndex = startIndex; arrayIndex 0 ) && (arrayIndex == startIndex)) {
    attrString = (CFMutableAttributedStringRef) ([ ((NSAttributedString*)currentElement) attributedSubstringFromRange:startRange] );
    }else {
    attrString = (CFMutableAttributedStringRef)currentElement;
    }
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, textBounds);
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
    CFRange fullAttributedStringRange = CFRangeMake(0, CFStringGetLength((CFStringRef)attrString));
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, fullAttributedStringRange, path, NULL);
    NSLog(@”Before drawing text the frame is: %@”, NSStringFromCGRect(textBounds));
    NSLog(@”For this text: %@”, [ (NSMutableAttributedString*)attrString string]);
    if (frame != NULL) {
    CTFrameDraw(frame, context);
    CFRange visibleRange = CTFrameGetVisibleStringRange(frame);
    if (visibleRange.length textBounds.size.height) {
    [_delegate insertNextPage: arrayIndex: NSMakeRange(0, 0)];
    break;
    }
    CGRect imageFrame = CGRectMake((textBounds.size.width – imgSize.width)/2, -textBounds.origin.y +25 , imgSize.width, imgSize.height);
    //NSLog(@”Image location is at %@ and image frame is :%@”,NSStringFromCGRect(textBounds), NSStringFromCGRect(imageFrame));
    UIImageView* imgvw = [ [ UIImageView alloc] initWithFrame: imageFrame];
    imgvw.image = currentImage;
    [self addSubview:imgvw];
    [imgvw release];
    textBounds.origin.y -= imgSize.height + 30 ;
    //textBounds.size.height += textBounds.origin.y;
    */
    }

    }
    if (arrayIndex == [arrayOfElements count] ) {
    [_delegate drawingFinished];
    }
    }

    //********************************************CODE********************************

    • Foobar Pig
      Posted July 29, 2010 at 23:54 | Permalink

      Sorry, but I don’t think that I can help you much here – I’m relatively new to all these CoreText functions and so far used it to draw only single style (same font) text, so.

      I would guess, that drawing out of screen would be caused by incorrectly set frames, but I think you have that stuff covered.

Leave a Reply