Since the introduction of iOS7 and the emphasis of content over decoration, text and its presentation on screen became even more important for creating appealing user interfaces.
The Text Kit framework provides a set of tools to process text and manage its layout. It’s easier to use than the low-level Core Text framework and it’s better integrated with the UIKit text handling classes (UITextField and UITextView).
The goal of this article is to show how to use Text Kit to create a simple custom layout in which the text flows around a shape inserted in a text view. The sample code is available for download here
Configure the Text Kit stack
Text Kit is built on a set of interdependent classes following the model-view-controller design pattern. As their NS prefix suggests, these classes were brought from the Cocoa framework and optimized for iOS.
The three classes that form the Text Kit stack are:
- NSTextStorage: is the model component of the stack. As a direct derivate of the NSAttributedString class, its purpose is to store the text and the properties defining its visual aspect and behavior
- NSTextContainer: is the view component of the stack. It’s not actually a UIView subclass, but it’s an abstract representation of the area where the text is drawn, usually inside a UITextView.
- NSLayoutManager: is the controller component of the stack. As any controller should do, it mediates the communication between the text storage and the text container. Its role is to transform the characters into glyphs, lay them out into the text container and repeat the process each time the stored text is modified.
The same NSTextStorage instance can be associated with multiple NSLayoutManager objects, thus allowing to create different appearances for a particular piece of text.
The layout manager can also be associated to multiple NSTextContainer objects which display the same content, with the same visual aspect defined by the layout manager, but on screen areas of different sizes and shapes, as configured by each text container instance.
Developers are able to interact with the layout process by inheriting from the Text Kit stack classes.
The process of creating a Text Kit stack is straightforward:
- instantiate the NSTextStorage object and initialize its contents and visual attributes, as for any NSAttributedString
- create the NSLayoutManager object and initialize its textStorage property with the previously instantiated text storage, or use the NSTextStorage -addLayoutManager method to create the association the other way around
- create the NSTextContainer object and associate it with the layout manager instance using the NSLayoutManager -addTextContainer: method or the NSTextContainer -addLayoutManager: method
These are precisely the steps I’ve taken to initialize the Text Kit stack in the code sample. You will notice that I used a NSTextStorage subclass, but I also could have used a NSTextStorage instance directly for this simple use case:
// Create the layout manager and associate it to the text storage
NSLayoutManager *customLayoutManager =[[NSLayoutManager alloc] init];
// Create the text container and associate it to the layout manager
NSTextContainer *customTextContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
// Initialize the text view with the previously created text container
self.textView = [[UITextView alloc] initWithFrame:self.view.bounds textContainer:customTextContainer];
self.textView.editable = NO;
// The text view extends to the screen edges
self.textView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[textView]|"
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[textView]|"
Define the areas not covered by text
The NSTextContainer class declares the public exclusionPaths property that can be initialized with a NSArray of UIBezierPath objects which represent the areas where text cannot be laid out.
Before laying out the text, the layout manager asks the associated text containers for the areas where text can be drawn. That is how the layout manager is able to ignore some regions of the container when it displays the text.
In the sample project, I initialized the exclusionPaths with a circular shape at the center of the screen. To make things easier for placing the shape in any device orientation, I inserted a square view behind the text view and center it using Auto Layout. Then, in the -viewDidLayoutSubviews method, I used the frame of this view to create the exclusion path:
self.textView.contentInset = UIEdgeInsetsMake(20.0f, 0.0f, 0.0f, 0.0f);
// Convert the excludedArea to the text view coordinates
CGRect excludedAreaInTextContainer = [self.textView convertRect:self.excludedArea.frame fromView:self.view];
UIBezierPath *exclusionPath = [UIBezierPath bezierPathWithOvalInRect:excludedAreaInTextContainer];
self.textView.textContainer.exclusionPaths = @[exclusionPath];
// Stroke the exclusion path
self.excludedShape = [CAShapeLayer layer];
self.excludedShape.path = exclusionPath.CGPath;
self.excludedShape.strokeColor = [UIColor blueColor].CGColor;
self.excludedShape.fillColor = nil;
self.excludedShape.lineWidth = 2.0f;
Even though this article studies a small feature of the Text Kit framework, it gives an idea about all the power it embeds, letting us process text and create unique and sophisticated text layouts with limited effort.