iOS Dynamic Type Without the Dynamism

10 October 2019

Back in 2013 when Apple released iOS 7, they introduced a typographical system of semantically named fonts that allows app developers to produce text in their apps that is sized and weighted appropriately for any given context. Apps can specify what the text they display represents (a heading, title, body text etc) and iOS vends an appropriate font for the job.

The font styles available as part of the iOS type system along with the properties of the fonts used to represent them.
The font styles available as part of the iOS type system along with the properties of the fonts used to represent them.

From Apple's Human Interface Guidelines:

The built-in text styles let you express content in ways that are visually distinct, while retaining optimal legibility. These styles are based on the system fonts and allow you to take advantage of key typographic features, such as Dynamic Type, which automatically adjusts tracking and leading for every font size.

Today we will be focussing on one of the typographical features of this system: Dyanmic Type.

What Is Dynamic Type?

Dynamic Type is an operating system level feature that allows users to specify how large they would like text to appear when using apps. A user with limited vision may request larger fonts be used to aid them when reading content. Alternatively, a user who wishes to see more content on the screen at any one time may choose a smaller font.

The iOS system settings screen that allows users to specify their desired text size.
The iOS system settings screen that allows users to specify their desired text size.

But Not All Type Should Be Dynamic

Again, from Apple's Human Interface Guidelines:

Prioritize content when responding to text-size changes. Not all content is equally important. When someone chooses a larger size, they want to make the content they care about easier to read; they don't always want every word on the screen to be larger.

As such, not all text in your app should be shown using a font that responds the user's system text size setting. The alternative is to use a font with a fixed point size that will not respond to the dynamic type system.

However, there are problems associated with this approach. The main problem relates to legibility. As shown in the font styles table above, fonts created using the semantic style names also have their leading and tracking adjusted in order to make them as legible as possible. If instead we simply create a system font with a fixed point size, we do not get these adjustments and as a result our text will not be as legible as it would have been had it been displayed using a font created with a semantic style name.

A secondary problem is that if we want our type to remain in proportion and visually balanced alongside type using a font that is created using a semantic style, we will have to duplicate the font sizes used by the dynamic type system in its default setting (17 point for body text, 13 point for a footnote etc). However, if Apple updates the Dynamic Type point sizes in a future iOS release, our fonts created with an explicit point size will no longer match the system styles.

Using Semantic Text Styles Without Dynamic Type Sizes

The solution to this problem is to use semantic text styles everywhere in our app, but disable Dynamic Type where we don't want it.

However, it may not be obvious at first how to achieve this. We access semantic text styles using the +[UIFont preferredFontForTextStyle:] API, which returns a font with the user's dynamic type setting applied. The secret is to use an API introduced in iOS 10, which is +[UIFont preferredFont​ForTextStyle:compatibleWith​TraitCollection:]. This version of the API takes a UITraitCollection as a second parameter, and trait collections contain a preferredContentSizeCategory property. It is this property which is used to determine how semantic text styles should be scaled. When using the standard +[UIFont preferredFontForTextStyle:]API, the system uses a default set of traits with a preferredContentSizeCategory matching the user's preference that they have configured in the system settings. However, using the new API, we can pass in our own set of traits to override this behaviour. If we would like a font to use for some body text that should not be scaled by the dynamic type system, we can create one like so:

let defaultContentSizeTraitCollection = UITraitCollection(preferredContentSizeCategory: .large)
let nonScalingFont = UIFont.preferredFont(forTextStyle: .body, compatibleWith: defaultContentSizeTraitCollection)

The default content size category, that is to say the one that is used when a user has not modified the system text size setting, is .large . By using a trait collection with this content size category we are returned fonts that match the settings given in the table at the top of this article, and have overcome the problems associated with using fonts of a fixed point size.

Tidying Things Up

While this approach works, creating fonts this way leads to a lot of duplicated trait collections and a rather long-winded piece of code for simply creating a font. Instead of this, we can make things much neater with a couple of extensions:

import UIKit

public extension UITraitCollection {
    /// A trait collection containing the default content size category used
    /// when the system does not have a modified type size set.
    static let defaultContentSizeTraitCollection = UITraitCollection(preferredContentSizeCategory: .large)
}

public extension UIFont {
    
    /// Returns an instance of the font associated with the text style, scaled
    /// appropriately for the default content size category used when the system
    /// does not have a modified type size set.
    ///
    /// - Parameter style: The text style required of the returned font.
    /// - Returns: The font that matches the given text style, scaled to the
    /// default system scaling.
    class func nonScalingPreferredFont(forTextStyle style: UIFont.TextStyle) -> UIFont {
        return preferredFont(forTextStyle: style, compatibleWith: .defaultContentSizeTraitCollection)
    }
}

This simplifies the creation of a non-scaling font to a simple one liner:

let nonScalingFont = UIFont.nonScalingPreferredFont(forTextStyle: .body)

Conclusion

Supporting Dynamic Type in your apps is incredibly important as without it your app is unlikely to be usable for visually impaired users. Following Apple's guidelines and carefully considering when and where your fonts should scale, and where they shouldn't, is the first step to properly supporting this technology. As we have seen, with the right know-how, creating fonts that work well together both when scaling and not scaling is a simple enough task that will allow your app to feel right at home on iOS for all of your users.