Auto Adjusting Fonts for Dynamic Type

It seems like a common theme of iOS 10 and Swift 3 is to remove boilerplate code. Here is a quick example where it is now easier to support dynamic type without the need to register an observer for the UIContentSizeCategoryDidChangeNotification. In passing we will also see some examples of the Swift 3 Grand Renaming.

Dynamic Type

Apple first introduced Dynamic Type with iOS 7. It allows the end user to specify their preferred text size from the device settings and is an important feature for making apps accessible to more users.

Text Size Setting

Adopting dynamic type is as simple as using the preferred font for a given text style. You can set this directly in Interface Builder or in code when setting up a view. The UIFont API has some minor readability improvements in Swift 3:

// Swift 2
headline.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
subhead.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
body.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
// Swift 3
headline.font = UIFont.preferredFont(forTextStyle: UIFontTextStyleHeadline)
subhead.font = UIFont.preferredFont(forTextStyle: UIFontTextStyleSubheadline)
body.font = UIFont.preferredFont(forTextStyle: UIFontTextStyleBody)

Note that there has been a bug (see rdar://25836850) stopping dynamic type working with the iOS 9 simulator since Xcode 7.3 which is not yet fixed with Xcode 8. The iOS 10 simulator is not affected.

To fully support dynamic type before iOS 10 you also need to observe the UIContentSizeCategoryDidChangeNotification to know when a user changes the text size. Typically you add an observer in the viewDidLoad method of a view controller (again note the changing naming conventions with Swift 3):

// Swift 2
NSNotificationCenter.defaultCenter().addObserver(self,
  selector:#selector(userChangedTextSize(_:)),
  name: UIContentSizeCategoryDidChangeNotification,
  object: nil)
// Swift 3
NotificationCenter.default().addObserver(self,
  selector:#selector(userChangedTextSize(notification:)),
  name: NSNotification.Name.UIContentSizeCategoryDidChange,
  object: nil)

In the target action for this notification you need to reset the font for each of the labels, text fields or text views in a view. Doing this for every view controller quickly becomes tedious…

adjustsFontsForContentSizeCategory

The first piece of good news is that iOS 10 introduces a new property on UILabel, UITextField and UITextView. Setting adjustsFontsForContentSizeCategory to true causes the font to update automatically when the user changes the text content size. You no longer need to observe the content size category did change notification.

headline.adjustsFontForContentSizeCategory = true
subhead.adjustsFontForContentSizeCategory = true
body.adjustsFontForContentSizeCategory = true

Starting from Xcode 8.3 beta 4 you can set adjustsFontForContentSizeCategory directly in Interface Builder:

Automatically Adjusts Font

That still requires iOS 10 so if you are supporting iOS 9 or earlier you will need to stick to listening for UIContentSizeCategoryDidChangeNotification.

Content Size Category Trait

The second piece of good news is that UITraitCollection class now has a preferredContentSizeCategory property. This means that if you do need to react when the user changes the text size you can override traitCollectionDidChange:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  if previousTraitCollection?.preferredContentSizeCategory !=
    traitCollection.preferredContentSizeCategory {
    // content size has changed
  }
}

Further Reading