Text Kit Tutorial: Getting Started

Gabriel Hauber
Achieve next-level typography with Text Kit!

Achieve next-level typography with Text Kit!

Update 12/12/14: Updated for Xcode 6.1.1.

Note from Ray: This is a Swift update to a popular Objective-C tutorial on our site, released as part of the iOS 8 Feast. Update by Gabriel Hauber, Original post by Tutorial Team member Colin Eberhardt. Enjoy!

The way that iOS renders text continues to grow more powerful over the years as Apple adds more features and capabilities. The release of iOS 7 brought with it some of the most significant text rendering changes yet. Now iOS 8 builds on that power, and makes it easier to use. A brief overview of iOS text editing might help you keep things in perspective.

In the old days before iOS 6, web views were usually the easiest way to render text with mixed styling, such as bold, italics, or even colors.

In 2012, iOS 6 added attributed string support to a number of UIKit controls. This made it much easier to achieve this type of layout without resorting to rendered HTML.

In iOS 6, UIKit controls based their text capabilities on both WebKit and Core Graphics’ string drawing functions, as illustrated in the hierarchical diagram below:

TextRenderingArchitecture-iOS6

Note: Does anything strike you as odd in this diagram? That’s right — UITextView uses WebKit under the hood. iOS 6 renders attributed strings on a text views as HTML, a fact that’s not readily apparent to developers who haven’t dug deeply into the framework.

Attributed strings in iOS 6 were indeed helpful for many use cases. However, for advanced layouts and multi-line rendered text, Core Text remained the only real option — a relatively low-level and cumbersome framework.

However, since iOS 7 there’s an easier way. With the current minimalistic design focus that eschews ornamentation and focuses more on typography — such as the UIButton that strips away all borders and shadows, leaving only text — it’s no surprise that iOS 7 added a whole new framework for working with text and text attributes: Text Kit.

The architecture is now much tidier; all of the text-based UIKit controls (apart from UIWebView) now use Text Kit, as shown in the following diagram:

TextRenderingArchitecture-iOS7

Text Kit is built on top of Core Text, inherits the full power of the Core Text framework, and to the delight of developers everywhere, wraps it in an improved object-oriented API.

In this Text Kit tutorial you’ll explore the various features of Text Kit as you create a simple yet feature-rich note-taking app for the iPhone that features reflowing text, dynamic text resizing, and on-the-fly text styling.

Ready to create something of note? :] Then read on to get started with Text Kit!

Note: At the time of writing this tutorial, our understanding is we cannot post screenshots of iOS 8 since it is still in beta. All the screenshots here are from iOS 7, which should look very close to what things will look like in iOS 8.

Getting Started

This tutorial includes a starter project with the user interface for the app pre-created so you can stay focused on Text Kit. Download the starter project archive to get started. Unzip the file, open the project in Xcode and build and run the app. It will look like the following:

Starter Image

The app creates an initial array of Note instances and renders them in a table view controller. Storyboards and segues detect cell selection in the table view and handle the transition to the view controller where users can edit the selected note.

Note: If you are new to Storyboards, check out Storyboards Tutorial in iOS 7. And if you’re subscribing to the video tutorials check out Video Tutorial Storyboards and Segues

Browse through the source code and play with the app a little to get a feel for the apps’ structure and how it functions. When you’re done with that, move on to the next section, which discusses the use of dynamic type in your app.

Dynamic Type

Dynamic type is one of the most game-changing features of iOS 7; it places the onus on your app to conform to user-selected font sizes and weights.

In iOS 7, open the Settings app and navigate to General/Accessibility and General/Text Size to view the settings that affect how the app displays text:

UserTextPreferences

In iOS 8, open the Settings app and navigate to General/Accessibility/Larger Text to access Dynamic Type text sizes.

iOS 7 offered the ability to enhance the legibility of text by increasing font weight, as well as an option to set the preferred font size for apps that support dynamic text.

Note: When Apple released Text Kit at WWDC 2013, they strongly encouraged developers to adopt Dynamic Type. At WWDC 2014 Apple went a bit further. They stressed that all built-in apps support Dynamic Type. Moreover, they made Dynamic Type much easier to use in iOS 8. While they didn’t threaten to break anyone’s legs if they didn’t get with the program, Apple did make their point forcefully. The WWDC 2014 session 226 What’s New in Tables and Collection Views, covers iOS 8 Dynamic Type support for table views and collections. You’d be well-advised to watch it! Users will expect apps written for iOS 7 and later to honor these settings, so ignore them at your own risk!

In order to make use of dynamic type you need to specify fonts using styles rather than explicitly stating the font name and size. iOS 7 added a new method to UIFont, preferredFontForTextStyle, that creates a font for the given style using the user’s font preferences.

The diagram below gives an example of each of the six different font styles:

TextStyles

The text on the left uses the smallest user selectable text size, the text in the center uses the largest, and the text on the right shows the effect of enabling the accessibility bold text feature.

Basic Support

Implementing basic support for dynamic text is relatively straightforward. Rather than using explicit fonts within your application, you instead request a font for a specific style. At runtime the app selects a suitable font based on the given style and the user’s text preferences.

With iOS 8, Apple made it far easier to implement Dynamic Type than was the case in iOS 7. In particular, default labels in table views support Dynamic Type automatically! Nonetheless, you may well want to support iOS 7, and/or you might want to use custom labels in your table views. So first you’ll learn how to handle Dynamic Type for iOS 7. Then you’ll discover how Apple makes your life even easier in iOS 8.

Why iOS 7 is Great, but iOS 8 is Even Greater

The starter project comes set with its deployment set to iOS 8. Before proceeding, build and run the app and try changing the default text size to various values. You will discover that both the text size and cell height in the table view list of notes changes accordingly. And you didn’t have to do a thing! But do observe also that the notes themselves do not reflect changes to the text size settings.

While not quite as wonderful, things are still pretty great in iOS 7. For most of this tutorial, it won’t matter if you’re using iOS 7 or iOS 8 (just be sure you’re using Xcode 6!), but for now, set the deployment level for the app to iOS 7 and follow along in the iOS simulator. And most of what follows is of use in iOS 8 too, so it’s worthwhile even if you do not plan to support iOS versions earlier than iOS 8.

Note: To set the deployment level to iOS 7 in Xcode 6, choose View/Navigators/Show Project Navigator. In the right hand panel, choose the project, click on info and select iOS 7 in the iOS Deployment Target popup menu. Also, select the Target in the right-hand panel and the set the Deployment Target to iOS 7.

It’s also important to make sure the simulator is acting as an iOS 7 device. So in the iOS Simulator choose Hardware/Device/iOS 7/iPhone 5s.

Now that you’re all set to run as an iOS 7 app, go ahead and build and run. Play with the text size settings as before and you’ll see that sadly enough, the app is ignoring your settings. Now, you will do something to make it work in iOS 7.

Open NoteEditorViewController.swift and add the following to the end of viewDidLoad:

textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

Notice you’re not specifying an exact font such as Helvetica Neue. Instead, you’re asking for an appropriate font for body text with the UIFontTextStyleBody text style constant.

Next, open NotesListViewController.swift and add the following to the tableView(_:cellForRowAtIndexPath:) method, just before the return call:

cell.textLabel?.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)

Again, you’re specifying a text style and iOS will return an appropriate font.

Using a semantic approach to font names, such as UIFontTextStyleSubHeadline, helps avoid hard-coded font names and styles throughout your code — and ensures that your app will respond properly to user-defined typography settings as expected.

Build and run the app again, and you’ll notice that the table view and the note screen now honor the current text size; the difference between the two is shown in the screenshots below:

That looks pretty good — but sharp readers will note that this is only half the solution. Head back to the Settings app under General/Text Size and modify the text size again. This time, switch back to SwiftTextKitNotepad — without re-launching the app — and you’ll notice that your app didn’t respond to the new text size.

Your users won’t take too kindly to that! Looks like that’s the first thing you need to correct in this app.

Responding to Updates

Open NoteEditorViewController.swift and add the following code to the end of viewDidLoad

NSNotificationCenter.defaultCenter().addObserver(self, 
    selector: "preferredContentSizeChanged:", 
    name: UIContentSizeCategoryDidChangeNotification,
    object: nil)

The above code registers the class to receive notifications when the preferred content size changes and passes in the method to be called (preferredContentSizeChanged) when this event occurs.

Next, add the following method to the class:

func preferredContentSizeChanged(notification: NSNotification) {
  textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
}

This simply sets the text view font to one based on the new preferred size.

Note: You might be wondering why it seems you’re setting the font to the same value it had before. When the user changes their preferred font size, you must request the preferred font again; it won’t update automatically. The font returned via preferredFontForTextStyle will be different when the font preferences are changed.

Open up NotesListViewController.swift and override the viewDidLoad function by adding the following code to the class:

override func viewDidLoad() {
  super.viewDidLoad()
  NSNotificationCenter.defaultCenter().addObserver(self,
      selector: "preferredContentSizeChanged:", 
      name: UIContentSizeCategoryDidChangeNotification, 
      object: nil)
}

Hey, isn’t that the same code you just added to NoteEditorViewController.swift? Yes, it is — but you’ll handle the preferred font change in a slightly different manner.

Add the following method to the class:

func preferredContentSizeChanged(notification: NSNotification) {
  tableView.reloadData()
}

The above code simply instructs the table view to reload its visible cells, which updates the appearance of each cell. This will trigger the calls to preferredFontForTextStyle() and refresh the font choice.

Build and run your app; change the text size setting, and verify that your app responds correctly to the new user preferences.

Changing Layout

That part seems to work well, but when you select a really small font size, your table view ends up looking a little sparse, as shown in the right-hand screenshot below:

ChangingLayout

This is one of the trickier aspects of dynamic type (in iOS 7). To ensure your application looks good across the range of font sizes, your layout needs to be responsive to the user’s text settings. Auto Layout solves a lot of problems for you, but this is one problem you’ll have to solve yourself.

Your table row height needs to change as the font size changes. Implementing the tableView(_:heightForRowAtIndexPath:) delegate method solves this quite nicely.

Add the following code to NotesListViewController.swift, in the section labelled Table view data source:

let label: UILabel = {
  let temporaryLabel = UILabel(frame: CGRect(x: 0, y: 0, width: Int.max, height: Int.max))
  temporaryLabel.text = "test"
  return temporaryLabel
}()
 
override func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
  label.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
  label.sizeToFit()
  return label.frame.height * 1.7
}

The above code creates a single shared instance of UILabel which the table view uses to calculate the height of the cell. Then, in tableView(_:heightForRowAtIndexPath:) you set the label’s font to be the same font used by the table view cell. It then invokes sizeToFit on the label, which forces the label’s frame to fit tightly around the text, and results in a frame height proportional to the table row height.

Build and run your app; modify the text size setting once more and the table rows now size dynamically to fit the text size, as shown in the screenshot below:

If you like, you may now reset the deployment to iOS 8 for the rest of the tutorial.

Letterpress Effect

The letterpress effect adds subtle shading and highlights to text that give it a sense of depth — much like the text has been slightly pressed into the screen.

Note: The term “letterpress” is a nod to early printing presses, which inked a set of letters carved on blocks and pressed them into the page. The letters often left a small indentation on the page — an unintended but visually pleasing effect, which is frequently replicated in digital typography today.

Open NotesListViewController.swift and replace tableView(_:cellForRowAtIndexPath:) with the following implementation:

override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell? {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
 
  let note = notes[indexPath.row]
  let font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
  let textColor = UIColor(red: 0.175, green: 0.458, blue: 0.831, alpha: 1)
  let attributes = [
    NSForegroundColorAttributeName : textColor,
    NSFontAttributeName : font,
    NSTextEffectAttributeName : NSTextEffectLetterpressStyle
  ]
  let attributedString = NSAttributedString(string: note.title, attributes: attributes)
 
  cell.textLabel?.attributedText = attributedString
 
  return cell
}

The above code creates an attributed string for the title of a table cell using the letterpress style.

Build and run your app; your table view will now display the text with a nice letterpress effect, as shown below:

Letterpress is a subtle effect — but that doesn’t mean you should overuse it! Visual effects may make your text more interesting, but they don’t necessarily make your text more legible.

Exclusion Paths

Flowing text around images or other objects is a standard feature of most word processors. Text Kit allows you to render text around complex paths and shapes with exclusion paths.

It would be handy to tell the user 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.

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 up NoteEditorViewController.swift and add the following property declaration to the class:

var timeView: TimeIndicatorView!

As the name suggests, this houses the time indicator subview.

Next, add this code to the very end of viewDidLoad:

timeView = TimeIndicatorView(date: note.timestamp)
textView.addSubview(timeView)

This simply creates an instance of the new view and adds it as a subview.

TimeIndicatorView calculates its own size, but it won’t do this automatically. You need a mechanism to call updateSize when the view controller lays out the subviews.

Finally, add the following two methods to the class:

override func viewDidLayoutSubviews() {
  updateTimeIndicatorFrame()
}
 
func updateTimeIndicatorFrame() {
  timeView.updateSize()
  timeView.frame = CGRectOffset(timeView.frame, textView.frame.width - timeView.frame.width, 0)
}

viewDidLayoutSubviews calls updateTimeIndicatorFrame, which does two things: it calls updateSize to set the size of the subview, and positions the subview in the top right corner of the text view.

All that’s left is to call updateTimeIndicatorFrame when your view controller receives a notification that the size of the content has changed. Replace the implementation of preferredContentSizeChanged to the following:

func preferredContentSizeChanged(notification: NSNotification) {
  textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
  updateTimeIndicatorFrame()
}

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 device Text Size preferences, and the view will automatically adjust to fit.

However, something doesn’t look quite right. The text of the note renders behind the time indicator view instead of flowing neatly around it. Fortunately, this is the exact problem that exclusion paths are designed to solve.

Exclusion Paths

Open TimeIndicatorView.swift and take a look at curvePathWithOrigin(). The time indicator view uses this code when filling its background, but you can also use it to determine the path around which you’ll flow your text. Aha — that’s why the calculation of the Bezier curve is broken out into its own method!

All that’s left is to define the exclusion path itself. Open up NoteEditorViewController.swift and add the following code block to the very end of updateTimeIndicatorFrame:

let exclusionPath = timeView.curvePathWithOrigin(timeView.center)
textView.textContainer.exclusionPaths = [exclusionPath]

The above code creates an exclusion path based on the Bezier path created in your time indicator view, but with an origin and coordinates that are relative to the text view.

Build and run your project and select an item from the list; the text now flows nicely around the time indicator view, as shown in the following screenshot:

ExclusionPath

This simple example only scratches the surface of the abilities of exclusion paths. You might notice that the exclusionPaths property expects an array of paths, meaning each container can support more than one exclusion path.

Furthermore, 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, exclusionPaths will handle it without problem!

As the text container notifies the layout manager when an exclusion path changes, you can implement dynamic or even animated exclusions paths — just don’t expect your user to appreciate the text moving around on the screen as they’re trying to read!

Dynamic Text Formatting and Storage

You’ve seen that Text Kit can dynamically adjust fonts based on the user’s text size preferences. But wouldn’t it be cool if fonts could update dynamically based on the actual text itself?

For example, what if 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:

TextKitStack

Behind the scenes, Apple creates these classes for you automatically 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. Let’s go over each class:

  • NSTextStorage stores the text it is to render as an attributed string, and informs the layout manager of any changes to the text’s contents. You might want to subclass NSTextStorage in order to dynamically change the text attributes as the text updates (as you will 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 might want to subclass NSTextContainer to define a complex shape that you would like to render text within.

To implement the dynamic text formatting feature in this app, you’ll need to subclass NSTextStorage in order to dynamically add text attributes as the user types in their text.

Once you’ve created your custom NSTextStorage, you’ll replace UITextView’s default text storage instance with your own implementation. Let’s give this a shot!

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 to the class:

override var string: String {
  return backingStore.string
}
 
override func attributesAtIndex(index: Int, effectiveRange range: NSRangePointer) -> [NSObject : AnyObject] {
  return backingStore.attributesAtIndex(index, effectiveRange: range)
}

The first of these two declarations overrides the string computed property, deferring to the backing store. Likewise the attributesAtIndex method also delegates to the backing store.

Finally add the remaining mandatory overrides to the same file:

override func replaceCharactersInRange(range: NSRange, withString str: String) {
  println("replaceCharactersInRange:\(range) withString:\(str)")
 
  beginEditing()
  backingStore.replaceCharactersInRange(range, withString:str)
  edited(.EditedCharacters | .EditedAttributes, range: range, changeInLength: (str as NSString).length - range.length)
  endEditing()
}
 
override func setAttributes(attrs: [NSObject : AnyObject]!, range: NSRange) {
  println("setAttributes:\(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 in order to notify its associated layout manager when making edits.

You’ve probably noticed that you need to write quite a bit of code in order to subclass text storage. Since NSTextStorage is a public interface of a class cluster, you can’t just subclass it and override a few methods to extend its functionality. Instead, there are certain requirements that you must implement yourself, such as the backing store for the attributed string data.

Note: Class clusters are a commonly used design pattern throughout Apple’s frameworks.

A class cluster is simply the Objective-C implementation of the Abstract Factory pattern, which provides a common interface for creating families of related or dependent objects without specifying the concrete classes. Familiar classes such as NSArray and NSNumber are in fact the public interface to a cluster of classes.

Apple uses class clusters to encapsulate private concrete subclasses under a public abstract superclass, and it’s this abstract superclass that declares the methods a client must use in order to create instances of its private subclasses. Clients are also completely unaware of which private class is being dispensed by the factory, since it only ever interacts with the public interface.

Using a class cluster certainly simplifies the interface, making it much easier to learn and use the class, but it’s important to note there’s been a trade-off between extensibility and simplicity. It’s often far more difficult to create a custom subclass of the abstract superclass of a cluster.

Now that you have a custom NSTextStorage, you need to make a UITextView that uses it.

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 instances 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, and locate the NoteEditorViewController view by expanding Detail Scene/Detail/View and select Text View. Delete this UITextView instance.

Next, open NoteEditorViewController.swift and remove the UITextView outlet from the class and replace it with the following property declarations:

var textView: UITextView!
var textStorage: SyntaxHighlightTextStorage!

These two properties are for your text view and the custom storage subclass.

Next remove the following lines from viewDidLoad:

textView.text = note.contents
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.swift, add the following method to the class:

func createTextView() {
  // 1. Create the text storage that backs the editor
  let attrs = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
  let attrString = NSAttributedString(string: note.contents, attributes: attrs)
  textStorage = SyntaxHighlightTextStorage()
  textStorage.appendAttributedString(attrString)
 
  let newTextViewRect = view.bounds
 
  // 2. Create the layout manager
  let layoutManager = NSLayoutManager()
 
  // 3. Create a text container
  let containerSize = CGSize(width: newTextViewRect.width, height: CGFloat.max)
  let container = NSTextContainer(size: containerSize)
  container.widthTracksTextView = true
  layoutManager.addTextContainer(container)
  textStorage.addLayoutManager(layoutManager)
 
  // 4. Create a UITextView
  textView = UITextView(frame: newTextViewRect, textContainer: container)
  textView.delegate = self
  view.addSubview(textView)
}

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

  1. Instantiate an instance of your custom text storage, and initialize it with an attributed string holding the content of the note.
  2. Create a layout manager.
  3. Create a text container and associate it with the layout manager. Then, associate the layout manager with the text storage.
  4. Create the actual text view with your custom text container, set the delegate, and add the text view 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.

Now still working in NoteEditorViewController.swift add the line below directly after the super.viewDidLoad() line in viewDidLoad:

createTextView()

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 = 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.swift and add the following method:

func applyStylesToRange(searchRange: NSRange) {
  // 1. create some fonts
  let fontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
  let boldFontDescriptor = fontDescriptor.fontDescriptorWithSymbolicTraits(.TraitBold)
  let boldFont = UIFont(descriptor: boldFontDescriptor, size: 0)
  let normalFont = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
 
  // 2. match items surrounded by asterisks
  let regexStr = "(\\*\\w+(\\s\\w+)*\\*)"
  let regex = NSRegularExpression(pattern: regexStr, options: nil, error: nil)!
  let boldAttributes = [NSFontAttributeName : boldFont]
  let normalAttributes = [NSFontAttributeName : normalFont]
 
  // 3. iterate over each match, making the text bold
  regex.enumerateMatchesInString(backingStore.string, options: nil, range: searchRange) {
    match, flags, stop in
    let matchRange = match.rangeAtIndex(1)
    self.addAttributes(boldAttributes, range: matchRange)
 
    // 4. reset the style to the original
    let maxRange = matchRange.location + matchRange.length
    if maxRange + 1 < self.length {
      self.addAttributes(normalAttributes, range: NSMakeRange(maxRange, 1))
    }
  }
}

The above code performs the following actions:

  1. Create 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. Create a regular expression (or regex) that locates any text surrounded by asterisks; for example, in the string “iOS 8 is *awesome* isn’t it?”, 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.
  3. Enumerate the matches returned by the regular expression and apply the bold attribute to each one.
  4. Reset 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.

Add the following method right after the code above:

func performReplacementsForRange(changedRange: NSRange) {
  var extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRangeForRange(NSMakeRange(changedRange.location, 0)))
  extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRangeForRange(NSMakeRange(NSMaxRange(changedRange), 0)))
  applyStylesToRange(extendedRange)
}

The code above expands the range that your code inspects when attempting to match your bold formatting pattern. This is required because changedRange typically indicates a single character; lineRangeForRange extends that range to the entire line of text.

Finally, add the following method right after the code above:

override func processEditing() {
  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.

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

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

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

Adding Further Styles

The basic principle of applying styles to delimited text is rather straightforward: use a regex to find and replace the delimited string using applyStylesToRange to set the desired style of the text.

Open SyntaxHighlightTextStorage.swift and add the following method to the class:

func createAttributesForFontStyle(style: String, withTrait trait: UIFontDescriptorSymbolicTraits) -> [NSObject : AnyObject] {
  let fontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
  let descriptorWithTrait = fontDescriptor.fontDescriptorWithSymbolicTraits(trait)
  let font = UIFont(descriptor: descriptorWithTrait, size: 0)
  return [NSFontAttributeName : font]
}

This method applies the supplied font style to the body font. It provides a zero size to the UIFont(descriptor:size:) constructor which forces UIFont to return a size that matches the user’s current font size preferences.

Next, add the following property and function to the class:

var replacements: [String : [NSObject : AnyObject]]!
 
func createHighlightPatterns() {
  let scriptFontDescriptor = UIFontDescriptor(fontAttributes: [UIFontDescriptorFamilyAttribute : "Zapfino"])
 
  // 1. base our script font on the preferred body font size
  let bodyFontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
  let bodyFontSize = bodyFontDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as NSNumber
  let scriptFont = UIFont(descriptor: scriptFontDescriptor, size: CGFloat(bodyFontSize.floatValue))
 
  // 2. create the attributes
  let boldAttributes = createAttributesForFontStyle(UIFontTextStyleBody, withTrait:.TraitBold)
  let italicAttributes = createAttributesForFontStyle(UIFontTextStyleBody, withTrait:.TraitItalic)
  let strikeThroughAttributes = [NSStrikethroughStyleAttributeName : 1]
  let scriptAttributes = [NSFontAttributeName : scriptFont]
  let redTextAttributes = [NSForegroundColorAttributeName : UIColor.redColor()]
 
  // construct a dictionary of replacements based on regexes
  replacements = [
    "(\\*\\w+(\\s\\w+)*\\*)" : boldAttributes,
    "(_\\w+(\\s\\w+)*_)" : italicAttributes,
    "([0-9]+\\.)\\s" : boldAttributes,
    "(-\\w+(\\s\\w+)*-)" : strikeThroughAttributes,
    "(~\\w+(\\s\\w+)*~)" : scriptAttributes,
    "\\s([A-Z]{2,})\\s" : redTextAttributes
  ]
}

Here’s what’s going on in this method:

  • First, create a “script” style using Zapfino as the font. Font descriptors help determine the current preferred body font size, which ensures the script font also honors the users’ preferred text size setting.
  • Next, construct the attributes to apply to each matched style pattern. You’ll cover createAttributesForFontStyle(withTrait:) in a moment; just park it for now.
  • Finally, create a dictionary that maps regular expressions to the attributes declared above.

If you’re not terribly familiar with regular expressions, the dictionary above might look a bit strange. But if you deconstruct the regular expressions that it contains, piece by piece, you can decode them without much effort.

Take the first regular expression you implemented above that matches words surrounded by asterisks:

(\\*\\w+(\\s\\w+)*\\*)

The double slashes are a result of having to escape special characters in literal strings with an extra backslash. If you cast out the escaping backslashes, and consider just the core regular expression, it looks like this:

(\*\w+(\s\w+)*\*)

Now, deconstruct the regular expression step by step:

  1. (\* – match an asterisk
  2. \w+ – followed by one or more “word” characters
  3. (\s\w+)* – followed by zero or more groups of spaces followed by “word” characters
  4. \*) – followed by an asterisk
Note: If you’d like to learn more about regular expressions above and beyond this tutorial, check out this NSRegularExpression tutorial and cheat sheet.

As an exercise, decode the other regular expressions yourself, using the explanation above and the cheat sheet as a guide. How many can you do on your own?

Here’s a question for you. Can you describe, in plain English, what the regular expression:

\s([A-Z]{2,})\s matches?

Solution Inside SelectShow

You will also need to initialize the replacements dictionary. Add the following class initializer to the SyntaxHighlightTextStorage class:

override init() {
	super.init()
	createHighlightPatterns()
}
 
required init(coder aDecoder: NSCoder) {
	super.init(coder: aDecoder)
}

You’re calling the plain initializer with no arguments in the rest of your project. The init(coder:) initializer is required to keep the compiler happy.

Finally, replace the implementation of applyStylesToRange() with the following:

func applyStylesToRange(searchRange: NSRange) {
  let normalAttrs = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
 
  // iterate over each replacement
  for (pattern, attributes) in replacements {
    let regex = NSRegularExpression(pattern: pattern, options: nil, error: nil)!
    regex.enumerateMatchesInString(backingStore.string, options: nil, range: searchRange) {
      match, flags, stop in
      // apply the style
      let matchRange = match.rangeAtIndex(1)
      self.addAttributes(attributes, range: matchRange)
 
      // reset the style to the original
      let maxRange = matchRange.location + matchRange.length
      if maxRange + 1 < self.length {
        self.addAttributes(normalAttrs, range: NSMakeRange(maxRange, 1))
      }
    }
  }
}

Previously, this method performed just one regex search for bold text. Now it does the same thing, but it iterates over the dictionary of regex matches and attributes since there are many text styles to look for. For each regex, it runs the search and applies the specified style to the matched pattern.

Note that the initialization of the NSRegularExpression can fail, so here it is implicitly unwrapped. If, for some reason, the pattern has an error in it resulting in a failed compilation of the pattern, the code will fail on this line, forcing you to fix the pattern, rather than failing further down.

Build and run your app, and exercise all of the new styles available to you, as illustrated below:

ExclusionPath

And here is a slightly more challenging exercise. If you enter the text: “*This is   not   bold*” (without the quotes) into a note, you’ll discover that it does not turn bold. In other words, if the selected text has multiple spaces between the words, there is no match.
Can you create a regular expression that will embolden that text? It’s just a simple modification of the one already in the code.

Solution Inside SelectShow

Your app is nearly complete; there are just a few loose ends to clean up.

If you’ve changed the orientation of your screen while working on your app, you’ve already noticed that the app no longer responds to content size changed notifications since your custom implementation doesn’t yet support this action.

As for the second issue, if you add a lot of text to a note you’ll notice that the bottom of the text view is partially obscured by the keyboard; it’s a little hard to type things when you can’t see what you’re typing!

Time to fix up those two issues.

Reviving Dynamic Type

To correct the issue with dynamic type, your code should update the fonts used by the attributed string containing the text of the note when the content size change notification occurs.

Add the following function to the SyntaxHighlightTextStorage class:

func update() {
  // update the highlight patterns
  createHighlightPatterns()
 
  // change the 'global' font
  let bodyFont = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
  addAttributes(bodyFont, range: NSMakeRange(0, length))
 
  // re-apply the regex matches
  applyStylesToRange(NSMakeRange(0, length))
}

The method above updates all the fonts associated with the various regular expressions, applies the body text style to the entire string, and then re-applies the highlighting styles.

Finally, open NoteEditorViewController.swift and modify preferredContentSizeChanged() to perform the update:

func preferredContentSizeChanged(notification: NSNotification) {
  textStorage.update()
  updateTimeIndicatorFrame()
}

Build and run; change your text size preferences, and the text should adjust accordingly as in the example below:

ExclusionPath

Resizing Text Views

All that’s left to do is solve the problem of the keyboard obscuring the bottom half of the text view when editing long notes. This is one issue that iOS 8 hasn’t solved for us yet!

To fix this, you’ll reduce the size of the text view frame when the keyboard is visible.
Open NoteEditorViewController.swift and add the following line to viewDidLoad(), right after the call to createTextView():

textView.scrollEnabled = true

This enables text view scrolling in your note editor view.

Now add the following code to the bottom of viewDidLoad():

NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidHide:", name: UIKeyboardDidHideNotification, object: nil)

This adds notifications for when the keyboard is shown or hidden; this is your signal to resize your text view frame accordingly.

Next, add the following method to the class:

func updateTextViewSizeForKeyboardHeight(keyboardHeight: CGFloat) {
  textView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height - keyboardHeight)
}

This method reduces the height of the text view to accommodate the keyboard.

Finally, you need to implement the two methods to respond to the notifications. Add the following methods to the class:

func keyboardDidShow(notification: NSNotification) {
  if let rectValue = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue {
    let keyboardSize = rectValue.CGRectValue().size
    updateTextViewSizeForKeyboardHeight(keyboardSize.height)
  }
}
 
func keyboardDidHide(notification: NSNotification) {
  updateTextViewSizeForKeyboardHeight(0)
}

When the keyboard is shown, you need to pull out the keyboard size from the notification, and from there it is simple to obtain the keyboard height. When the keyboard is hidden you just need to reset the height adjustment back to zero.

Note: Although on earlier versions of iOS you needed to account for the current screen orientation when calculating the new text view size (because the width and height properties of UIView instances are swapped when the screen orientation changes, but the keyboard’s width and height properties are not!), this is no longer required on iOS 8.

Build and run your app, edit a note and check that displaying the keyboard no longer obscures the text, as shown below:

Keyboard Shown

Note: At the time of writing there is a subtle bug with iOS 8 – when the text view resizes, the cursor position may still be off-screen. The cursor moves to its correct location if the user taps the ‘return’ key. We’ll keep an eye on this, and if the bug persists we’ll try to find an alternative solution.

Where To Go From Here?

Hopefully, this Text Kit tutorial has helped you understand the various new features such as dynamic type, font descriptors and letterpress, that you will no doubt find use for in practically ever app you write. You can download the final project and review the code and the app.

If you’d like to learn more about Text Kit, check out our book iOS 7 By Tutorials. The book has two chapters on Text Kit that provide an in-depth look at the Text Kit architecture and will show you how to create high performance multi-column text layouts.

We hope you enjoyed this tutorial, and if you have any questions or comments please join the forum discussion below!

Gabriel Hauber

I am an indie iOS developer living on the beautiful Sunshine Coast in Queensland, Australia. Besides writing code in Swift and Objective-C, I still occasionally write some Java… but I also dabble in novel writing (I've participated several times in the annual NaNoWriMo craziness!), photography and music. My "flagship" app on the iOS App Store is SongSheet for the iPad.

Other Items of Interest

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Come check out Alt U

Our Books

Our Team

Video Team

... 9 total!

Swift Team

... 15 total!

iOS Team

... 47 total!

Android Team

... 15 total!

OS X Team

... 12 total!

Apple Game Frameworks Team

... 15 total!

Unity Team

... 11 total!

Articles Team

... 8 total!