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.

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

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:

Time View

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.

Time View with Exclusion Path

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.

Textview Styles

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:

TextKitStack

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 subclass NSTextStorage 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 a UITextView. You can subclass NSTextContainer 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.