Text Kit Tutorial
Learn how to easily layout your text in iOS 7 in this Text Kit tutorial! By Colin Eberhardt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Text Kit Tutorial
45 mins
- Getting started
- Dynamic type
- Basic support
- Responding to updates
- Changing layout
- Letterpress effects
- Exclusion paths
- Adding the view
- Exclusion paths
- Dynamic text formatting and storage
- Subclassing NSTextStorage
- A UITextView with a custom Text Kit stack
- Dynamic formatting
- Adding further styles
- Reviving dynamic type
- Resizing text views
- Where To Go From Here?
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:
- An instance of your custom text storage is instantiated and initialized with an attributed string holding the content of the note.
- A layout manager is created.
- A text container is created and associated with the layout manager. The layout manager is then associated with the text storage.
- 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:
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:
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:
- 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.
- 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. - Enumerates the matches returned by the regular expression and applies the bold attribute to each one.
- 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:
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!