Text Kit Tutorial

Learn how to easily layout your text in iOS 7 in this Text Kit tutorial! By Colin Eberhardt.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 6 of this article. Click here to view the first page.

A UITextView with a custom Text Kit stack

Instantiating UITextView from the storyboard editor automatically creates an instance of NSTextStorage, NSLayoutManager and NSTextContainer (i.e. the Text Kit stack) and exposes all three as read-only properties.

There is no way to change these from the storyboard editor, but luckily you can if you create the UITextView and Text Kit stack programatically.

Let’s give this a shot. Open up Main.storyboard in Interface Builder and locate the NoteEditorViewController view. Delete the UITextView instance.

Next, open NoteEditorViewController.m and remove the UITextView outlet from the class extension.

At the top of NoteEditorViewController.m, import the text storage implementation as follows:

#import "SyntaxHighlightTextStorage.h"

Add the following code immediately after the TimeIndicatorView instance variable in NoteEditorViewController.m:

SyntaxHighlightTextStorage* _textStorage;
UITextView* _textView;

These are two instance variables for your text storage subclass, and a text view that you will create programmatically soon.

Next remove the following lines from viewDidLoad in NoteEditorViewController.m:

self.textView.text = self.note.contents;
self.textView.delegate = self;
self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];

Since you are no longer using the outlet for the text view and will be creating one manually instead, you no longer need these lines.

Still working in NoteEditorViewController.m, add the following method:

- (void)createTextView
{
    // 1. Create the text storage that backs the editor
    NSDictionary* attrs = @{NSFontAttributeName:
        [UIFont preferredFontForTextStyle:UIFontTextStyleBody]};
    NSAttributedString* attrString = [[NSAttributedString alloc]
                                   initWithString:_note.contents
                                       attributes:attrs];
    _textStorage = [SyntaxHighlightTextStorage new];
    [_textStorage appendAttributedString:attrString];
    
    CGRect newTextViewRect = self.view.bounds;
    
    // 2. Create the layout manager
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    
    // 3. Create a text container
    CGSize containerSize = CGSizeMake(newTextViewRect.size.width,  CGFLOAT_MAX);
    NSTextContainer *container = [[NSTextContainer alloc] initWithSize:containerSize];
    container.widthTracksTextView = YES;
    [layoutManager addTextContainer:container];
    [_textStorage addLayoutManager:layoutManager];
    
    // 4. Create a UITextView
    _textView = [[UITextView alloc] initWithFrame:newTextViewRect
                                    textContainer:container];
    _textView.delegate = self;
    [self.view addSubview:_textView];
}

This is quite a lot of code. Let’s consider each step in turn:

  1. An instance of your custom text storage is instantiated and initialized with an attributed string holding the content of the note.
  2. A layout manager is created.
  3. A text container is created and associated with the layout manager. The layout manager is then associated with the text storage.
  4. Finally the actual text view is created with your custom text container, the delegate set and the text view added as a subview.

At this point the earlier diagram, and the relationship it shows between the four key classes (storage, layout manager, container and text view) should make more sense:

TextKitStack

Note that the text container has a width matching the view width, but has infinite height — or as close as CGFLOAT_MAX can come to infinity. In any case, this is more than enough to allow the UITextView to scroll and accommodate long passages of text.

Within viewDidLoad add the following line just after the call to viewDidLoad on the superclass:

[self createTextView]; 

Next modify the first line of preferredContentSizeChanged to read as follows:

_textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];

Here you simply replace the old outlet property with the new instance variable.

One last thing, a custom view created in code doesn’t automatically inherit the layout constraints set in the storyboard; therefore, the frame of your new view won’t resize when the device orientation changes. You’ll need to explicitly set the frame yourself.

To do this, add the following line to the end of viewDidLayoutSubviews:

_textView.frame = self.view.bounds;

Build and run your app; open a note and edit the text while keeping an eye on the Xcode console. You should see a flurry of log messages created as you type, as below:

LogMessages

This is simply the logging code from within SyntaxHighlightTextStorage to give you an indication that your custom text handling code is actually being called.

The basic foundation of your text parser seems fairly solid — now to add the dynamic formatting.

Dynamic formatting

In this next step you are going to modify your custom text storage to embolden text *surrounded by asterisks*.

Open SyntaxHighlightTextStorage.m and add the following method:

-(void)processEditing
{
    [self performReplacementsForRange:[self editedRange]];
    [super processEditing];
}

processEditing sends notifications for when the text changes to the layout manager. It also serves as a convenient home for any post-editing logic.

Add the following method right after processEditing:

- (void)performReplacementsForRange:(NSRange)changedRange
{
    NSRange extendedRange = NSUnionRange(changedRange, [[_backingStore string]
                             lineRangeForRange:NSMakeRange(changedRange.location, 0)]);
    extendedRange = NSUnionRange(changedRange, [[_backingStore string] 
                          lineRangeForRange:NSMakeRange(NSMaxRange(changedRange), 0)]);
    [self applyStylesToRange:extendedRange];
}

The code above expands the range that will be inspected to match our bold formatting pattern. This is required because changedRange typically indicates a single character; lineRangeForRange extends that range to the entire line of text.

Add the following method right after performReplacementsForRange:

- (void)applyStylesToRange:(NSRange)searchRange
{
    // 1. create some fonts
    UIFontDescriptor* fontDescriptor = [UIFontDescriptor
                             preferredFontDescriptorWithTextStyle:UIFontTextStyleBody];
    UIFontDescriptor* boldFontDescriptor = [fontDescriptor
                           fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
    UIFont* boldFont =  [UIFont fontWithDescriptor:boldFontDescriptor size: 0.0];
    UIFont* normalFont =  [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
    
    // 2. match items surrounded by asterisks
    NSString* regexStr = @"(\\*\\w+(\\s\\w+)*\\*)\\s";
    NSRegularExpression* regex = [NSRegularExpression
                                   regularExpressionWithPattern:regexStr
                                                        options:0
                                                          error:nil];
    
    NSDictionary* boldAttributes = @{ NSFontAttributeName : boldFont };
    NSDictionary* normalAttributes = @{ NSFontAttributeName : normalFont };
    
    // 3. iterate over each match, making the text bold
    [regex enumerateMatchesInString:[_backingStore string]
              options:0
                range:searchRange
           usingBlock:^(NSTextCheckingResult *match,
                        NSMatchingFlags flags,
                        BOOL *stop){
        
        NSRange matchRange = [match rangeAtIndex:1];
        [self addAttributes:boldAttributes range:matchRange];
        
        // 4. reset the style to the original
        if (NSMaxRange(matchRange)+1 < self.length) {
            [self addAttributes:normalAttributes
                range:NSMakeRange(NSMaxRange(matchRange)+1, 1)];
        }
    }];
} 

The above code performs the following actions:

  1. Creates a bold and a normal font for formatting the text using font descriptors. Font descriptors help you avoid the use of hardcoded font strings to set font types and styles.
  2. Creates a regular expression (or regex) that locates any text surrounded by asterisks; for example, in the string “iOS 7 is *awesome*”, the regular expression stored in regExStr above will match and return the text “*awesome*”. Don’t worry if you’re not totally familiar with regular expressions; they’re covered in a bit more detail later on in this chapter.
  3. Enumerates the matches returned by the regular expression and applies the bold attribute to each one.
  4. Resets the text style of the character that follows the final asterisk in the matched string to “normal”. This ensures that any text added after the closing asterisk is not rendered in bold type.

Note: Font descriptors are a type of descriptor language that allows you to modify fonts by applying specific attributes, or to obtain details of font metrics, without the need to instantiate an instance of UIFont.

Build and run your app; type some text into a note and surround one of the words with asterisks. The words will be automagically bolded, as shown in the screenshot below:

BoldText

That’s pretty handy — you’re likely thinking of all the other styles that could be added to your text.

You’re in luck: the next section shows you how to do just that!

Colin Eberhardt

Contributors

Colin Eberhardt

Author

Over 300 content creators. Join our team.