iOS Text Kit Basics

(reposted from Giant Robots Smashing Into Other Giant Robots, where it originally appeared May 30, 2014)

In the iOS 7 SDK, Apple gave developers programmatic access to Text Kit, an advanced text layout system that lets you easily display and edit rich text in your applications. Although these capabilities are new to iOS, they’ve existed in other contexts (OS X since its first release, and OpenStep before that) for nearly two decades, in what’s called the Cocoa text system. In fact, if you’ve used OS X at all, there is a near 100% chance that you’ve run apps built with the Cocoa text system. However, for iOS developers, who often are not steeped in the details of OS X development, the details of using the supplied text layout system are new, and may seem mysterious at first. I intend to help you understand the basics of how this works, and see how you can add rich text features to your own apps.

The simplest thing

Let’s start off by making the simplest possible app that shows off some of what Text Kit can do. In Xcode, create a new project using the Single View Application template, and name it Simple Text View. Select Main.storyboard, use the Object Library to find a UITextView object, and drag it out to fill the view of the available view controller; you’ll see blue guidelines appear and the whole thing snap into place when it’s properly centered. Then use the Attributes Inspector to change the text view’s Text attribute from Plain to Attributed. What this does is tell the text view to allow rich text by using an attributed string. An attributed string, which is represented in iOS by the NSAttributedString class, is simply a string that has some attached metadata describing its attributes. This metadata may contain any number of ranges of characters, each with its own set of attributes. For example, you could specify that starting at the fifth character, the next six characters are bold, and that starting at the tenth character, the next five characters are italicized; In that case, the tenth character would be both bold and italicized. In effect,

0123456789ABCDEF

However, plenty of rich text content is created not by programmatically specifying ranges and attributes, but by users working in an editor that lets them create rich text. That’s a use case that is fully supported by UITextView starting in iOS 7.

UITextView in Interface Builder

To prove this, use the Attributes Inspector to modify parts of the “Lorem ipsum” text that the view contains by default. Use the controls in the inspector to change some fonts, adjust paragraph alignment, set foreground and backgroud colors, whatever you want. When you hit cmd-R to run the app in the iOS Simulator or on a device, you’ll see that all the formatting changes you made show up on the device. You can tap to edit the text at any point, and the formatting that applies where the cursor is will carry on to new characters you type, just as you’d expect from any word processor application.

The innards

So far, so good. Even better, it turns out that a few other popular UIKit classes, namely UILabel and UITextField, also allow the use of attributed strings in iOS 7. This means that if you just want to display some rich text in a single rectangular box, you’re all set. Just put a properly configured UILabel where you want to show your rich text, and you’re done! This simple task was remarkably hard to accomplish before iOS 7, so right there we’ve made a huge leap.

But, what if you want to do more? There are certain kinds of layout tricks that none of the UIKit classes can do on their own, out of the box. For example, if you want to make text flow around a graphic, or make a single string fill up one rectangle before spilling into another (as in the case of multiple columns), you’ll have to do more. Fortunately, the innards of Text Kit, which are used by UITextView and the rest, are at your disposal in the form of the NSTextStorage, NSLayoutManager, and NSTextContainer classes. Let’s talk about these one by one:

A class like UITextView uses these components to do all its text layout. In fact, UITextView has three properties called textStorage, textContainer, and layoutManager for just this purpose. When UITextView wants to draw its content, it tells its layoutManager to figure out which glyphs (the graphical representations of the characters it contains) from its textStorage can fit within its textContainer, then it tells the layoutManager to actually draw those glyphs at a point inside the text view’s frame. So you see that the design of UITextView itself is inherently limited to a single rectangle. In order to get a feel for how these innards work, I’ll now show you a UIView subclass that will display rich text in multiple columns, a trick that UITextView really can’t pull off in its current form.

Create TBTMultiColumnTextView

In your open Xcode project, create a new subclass of UIView called TBTMultiColumnView. Like UITextView, this class will have textStorage and layoutManager properties. Unlike UITextView, it will keep track of multiple independent text containers and multiple origins for drawing rectangles. The first thing you should do is create a class extension at the top of the file, containing the following properties:

@interface TBTMultiColumnTextView ()

@property (copy, nonatomic) NSTextStorage *textStorage;
@property (strong, nonatomic) NSArray *textOrigins;
@property (strong, nonatomic) NSLayoutManager *layoutManager;

@end

Besides the NSTextStorage and NSLayoutManager instances, we’re also going to maintain an array of origins, each corresponding to an NSTextContainer. We don’t have to hang onto the text containers themselves, because the layout manager keeps its own list, which we can access.

Now, let’s get started with the methods for this class. First, override viewDidLoad as shown here:

- (void)awakeFromNib {
    [super awakeFromNib];
    self.layoutManager = [[NSLayoutManager alloc] init];
    
    NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"constitution"
                                             withExtension:@"rtf"];
    self.textStorage = [[NSTextStorage alloc] initWithFileURL:fileURL
                                                      options:
                        @{NSDocumentTypeDocumentAttribute:NSRTFTextDocumentType}
                                           documentAttributes:nil
                                                        error:nil];
    [self createColumns];
}

This method is pretty straightforward. It starts off by creating a layout manager, which we’ll use every time we need to draw this object’s content. Then we read the contents of an RTF file, which we’ve included in our project, into an NSTextStorage instance. Our project contains an RTF file that contains the U.S. constitution, but you can use any RTF document you have at hand. Since this object will need to be redrawn any time the text storage changes, we implement the setter, like this:

- (void)setTextStorage:(NSTextStorage *)textStorage {
    _textStorage = [[NSTextStorage alloc] initWithAttributedString:textStorage];
    [self.textStorage addLayoutManager:self.layoutManager];
    [self setNeedsDisplay];
}

Note that we have a special way of making a new copy of the object that’s passed in. As it turns out, just sending copy to an instance of NSTextStorage actually returns an instance of an immutable parent class (just like you’d expect with, say, an NSMutableString). That’s why we take the step of explicitly creating a new instance based on the received parameter.

At the end of awakeFromNib, we called the createColumns method, which is where most of this class’s work really happens. It looks like this:

- (void)createColumns {
    // Remove any existing text containers, since we will recreate them.
    for (NSUInteger i = [self.layoutManager.textContainers count]; i > 0;) {
        [self.layoutManager removeTextContainerAtIndex:--i];
    }
    
    // Capture some frequently-used geometry values in local variables.
    CGRect bounds = self.bounds;
    CGFloat x = bounds.origin.x;
    CGFloat y = bounds.origin.y;
    
    // These are effectively constants. If you want to make this class more
    // extensible, turning these into public properties would be a nice start!
    NSUInteger columnCount = 2;
    CGFloat interColumnMargin = 10;
    
    // Calculate sizes for building a series of text containers.
    CGFloat totalMargin = interColumnMargin * (columnCount - 1);
    CGFloat columnWidth = (bounds.size.width - totalMargin) / columnCount;
    CGSize columnSize = CGSizeMake(columnWidth, bounds.size.height);
    
    NSMutableArray *containers = [NSMutableArray arrayWithCapacity:columnCount];
    NSMutableArray *origins = [NSMutableArray arrayWithCapacity:columnCount];
    
    for (NSUInteger i = 0; i < columnCount; i++) {
        // Create a new container of the appropriate size, and add it to our array.
        NSTextContainer *container = [[NSTextContainer alloc] initWithSize:columnSize];
        [containers addObject:container];
        
        // Create a new origin point for the container we just added.
        NSValue *originValue = [NSValue valueWithCGPoint:CGPointMake(x, y)];
        [origins addObject:originValue];
        
        [self.layoutManager addTextContainer:container];
        x += columnWidth + interColumnMargin;
    }
    self.textOrigins = origins;
}

This method is honestly a little longer than we’d like, but for this example it does the job. This method may need to be called multiple times, whenever the view’s coordinates are adjusted (such as when the device rotates), so we need to make sure it can run multiple times without ending up in a weird state. So, it starts off by removing any old text containers that may be attached to the layout manager. It does this because the whole point of this method is to create a fresh set of text containers, and having old ones lying around will only give us grief. This method then calculates appropriate text container sizes depending on the view’s size and some hard-coded values for the number of columns and the amount of margin that should appear between columns. Finally it creates and configures a number of containers and and equal number of points (wrapped in NSValue objects).

Next we’re going to make use of all those containers and points we just created. The drawRect: method tells the layout manager to finally draw its content into each text container. It looks like this:

- (void)drawRect:(CGRect)rect {
    for (NSUInteger i = 0; i < [self.layoutManager.textContainers count]; i++) {
        NSTextContainer *container = self.layoutManager.textContainers[i];
        CGPoint origin = [self.textOrigins[i] CGPointValue];
        
        NSRange glyphRange = [self.layoutManager glyphRangeForTextContainer:container];
        
        [self.layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:origin];
    }
}

All we do here is loop over all available text containers and origin points, each time asking the layout manager which glyphs it can fit into the container, then telling the layout manager to draw those glyphs at the origin point.

That’s just about all we need. In order to make things work after device rotation, however, we need to do one more thing. By overriding layoutSubviews, which is called when the view rotates, we can make sure that the columns are regenerated for the new size:

- (void)layoutSubviews {
    [super layoutSubviews];
    [self createColumns];
    [self setNeedsDisplay];
}

That’s all we need to make this class draw rich text in two columns, and automatically adjust for changes in view geometry. To see this in action, go back to the storyboad and follow these steps:

Once you’ve taken those final steps, you can build and run in the simulator or on a device, and see your multicolumn display in all its glory!

Multi Column Text View running on iOS
Simulator

Closing remarks

This class demonstrates a technique for creating a view that lets rich text flow across multiple columns in just a few lines of code. But we’re really just scratching the surface here. Besides flowing across multiple rectangles, Text Kit will let you do plenty of other things, including drawing text inside the path of an arbitrary shape, making text flow around other paths, and more. You can learn more about thse techniques by looking at Apple’s iOS Text Kit Overview, as well as their Mac documentation for the Cocoa text system, which is where much of Text Kit’s functionality originated.

Comments