Text Kit Tutorial: Getting Started
In this tutorial, you’ll learn how to use Text Kit in your iOS app to layout your text and create different visual styles. By Bill Morefield.
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
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: Getting Started
30 mins
- Getting Started
- Understanding Dynamic Type
- Implementing Basic Support
- Responding to Updates
- Changing Layout
- Implementing Letterpress Effect
- Creating Exclusion Paths
- Adding the View
- Adding Exclusion Paths
- Leveraging Dynamic Text Formatting and Storage
- Subclassing NSTextStorage
- Implementing UITextView With a Custom Text Kit Stack
- Adding Dynamic Formatting
- Adding Further Styles
- Reviving Dynamic Type
- Where to Go From Here?
Creating Exclusion Paths
Flowing text around images and other objects is a commonly needed styling feature. Text Kit allows you to render text around complex paths and shapes with exclusion paths.
It would be handy to show the note’s creation date. You’re going to add a small curved view to the top right-hand corner of the note that shows this information. This view is already implemented for you in the starter project. You can have a look it at TimeIndicatorView.swift.
You’ll start by adding the view itself. Then you’ll create an exclusion path to make the text wrap around it.
Adding the View
Open NoteEditorViewController.swift and add the following property declaration for the time indicator subview to the class:
var timeView: TimeIndicatorView!
Next, add this code to the very end of viewDidLoad()
:
timeView = TimeIndicatorView(date: note.timestamp)
textView.addSubview(timeView)
This creates an instance of the new view and adds it as a subview.
TimeIndicatorView
calculates its own size, but it won’t automatically do this. You need a mechanism to change its size when the view controller lays out the subviews.
To do that, add the following two methods to the class:
override func viewDidLayoutSubviews() {
updateTimeIndicatorFrame()
}
func updateTimeIndicatorFrame() {
timeView.updateSize()
timeView.frame = timeView.frame
.offsetBy(dx: textView.frame.width - timeView.frame.width, dy: 0)
}
The system calls viewDidLayoutSubviews()
when the view dimensions change. When that happens, you call updateTimeIndicatorFrame()
, which then invokes updateSize()
to set the size of the subview and place it in the top right corner of the text view.
Build and run your project. Tap on a list item, and the time indicator view will display in the top right-hand corner of the item view, as shown below:
Modify the Text Size preferences, and the view will adjust to fit.
But something doesn’t look quite right. The text of the note renders behind the time indicator instead of flowing around it. This is the problem that exclusion paths solve.
Adding Exclusion Paths
Open TimeIndicatorView.swift and take look at curvePathWithOrigin(_:)
. The time indicator view uses this code when filling its background. You can also use it to determine the path around which you’ll flow your text. That’s why the calculation of the Bezier curve is broken out into its own method.
Open NoteEditorViewController.swift and add the following code to the very end of updateTimeIndicatorFrame()
:
let exclusionPath = timeView.curvePathWithOrigin(timeView.center)
textView.textContainer.exclusionPaths = [exclusionPath]
This code creates an exclusion path based on the Bezier path in your time indicator view, but with an origin and coordinates relative to the text view.
Build and run your project. Now, select an item from the list. The text now flows around the time indicator view.
This example only scratches the surface of the capabilities of exclusion paths. Notice that the exclusionPaths
property expects an array of paths, meaning each container can support multiple exclusion paths. Exclusion paths can be as simple or as complicated as you want. Need to render text in the shape of a star or a butterfly? As long as you can define the path, exclusion paths will handle it without problem.
Leveraging Dynamic Text Formatting and Storage
You’ve seen that Text Kit can dynamically adjust fonts based on the user’s text size preferences. Wouldn’t it be cool if fonts could update based on the text itself?
For example, say you want to make this app automatically:
- Make any text surrounded by the tilde character (~) a fancy font.
- Make any text surrounded by the underscore character (_) italic.
- Make any text surrounded by the dash character (-) crossed out.
- Make any text in all caps colored red.
That’s exactly what you’ll do in this section by leveraging the power of the Text Kit framework!
To do this, you’ll need to understand how the text storage system in Text Kit works. Here’s a diagram that shows the “Text Kit stack” used to store, render and display text:
Behind the scenes, Apple automatically creates these classes for when you create a UITextView
, UILabel
or UITextField
. In your apps, you can either use these default implementations or customize any part to get your own behavior. Going over each class:
-
NSTextStorage
stores the text it is to render as an attributed string, and it informs the layout manager of any changes to the text’s contents. You can subclassNSTextStorage
in order to dynamically change the text attributes as the text updates (as you’ll see later in this tutorial). -
NSLayoutManager
takes the stored text and renders it on the screen. It serves as the layout ‘engine’ in your app. -
NSTextContainer
describes the geometry of an area of the screen where the app renders text. Each text container is typically associated with aUITextView
. You can subclassNSTextContainer
to define a complex shape that you would like to render text within.
You’ll need to subclass NSTextStorage
in order to dynamically add text attributes as the user types in text. Once you’ve created your custom NSTextStorage
, you’ll replace UITextView
’s default text storage instance with your own implementation.
Subclassing NSTextStorage
Right-click on the SwiftTextKitNotepad group in the project navigator, select New File…, and choose iOS/Source/Cocoa Touch Class and click Next.
Name the class SyntaxHighlightTextStorage, make it a subclass of NSTextStorage, and confirm that the Language is set to Swift. Click Next, then Create.
Open SyntaxHighlightTextStorage.swift and add a new property inside the class declaration:
let backingStore = NSMutableAttributedString()
A text storage subclass must provide its own persistence, hence the use of a NSMutableAttributedString
backing store — more on this later.
Next, add the following code to the class:
override var string: String {
return backingStore.string
}
override func attributes(
at location: Int,
effectiveRange range: NSRangePointer?
) -> [NSAttributedString.Key: Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
The first of these two declarations overrides the string
computed property, deferring to the backing store. Likewise the attributes(at: location)
method also delegates to the backing store.
Finally, add the remaining mandatory overrides to the same file:
override func replaceCharacters(in range: NSRange, with str: String) {
print("replaceCharactersInRange:\(range) withString:\(str)")
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range,
changeInLength: (str as NSString).length - range.length)
endEditing()
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
print("setAttributes:\(String(describing: attrs)) range:\(range)")
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
Again, these methods delegate to the backing store. However, they also surround the edits with calls to beginEditing()
, edited()
and endEditing()
. The text storage class requires these three methods to notify its associated layout manager when making edits.
Now that you have a custom NSTextStorage
, you need to make a UITextView
that uses it.