Engineering

Dynamic Type & In-App Font Scaling

Our guide to supporting custom fonts & accessibility

The Dynamic Type feature allows users to choose the size of textual content displayed on the screen. It helps users who need larger text for better readability. It also accommodates those who can read smaller text, allowing more information to appear on the screen. Apps that support Dynamic Type also provide a more consistent reading experience.

From Apple’s Developer Documentation.

Since iOS 7, Apple has provided users with the ability to adjust the size of displayed content in your apps. Most content-driven apps support this feature seamlessly. This provides great support for every reader, but what if you want to provide extended scaling? What occurs behind the scenes, and what caveats (if any) are presented? You’re probably here because you need to support Dynamic Type or possibly build your own font scaling system, and we can help you.

First, let’s take a look at the weight, size, and leading values for each text style in the default content category size (Large). The text style will determine the scale factor needed to support Dynamic Type.

A graph showing weight, size, and leading values for each text style in Apple's Human Interface Guidelines on Typography

Weight, size, and leading values for each text style at the default Dynamic Type size from Apple’s Human Interface Guidelines on Typography.

Next, let’s take a look at the Dynamic Type settings in iOS Settings, which can be found either under Accessibility or Display & Brightness. The “Larger Text” setting below is in Accessibility → Display & Text Size → Larger Text.

A screenshot from the iOS Settings app showing the "Larger Accessibility Sizes" option in the "Larger Text" menu turned on.

Setting up Dynamic Type

Luckily, in order to support Dynamic Type with system fonts, all we need are a few lines:

let textLabel = UILabel()
textLabel.font = .preferredFont(forTextStyle: .headline)
textLabel.adjustsFontForContentSizeCategory = true

We made an Xcode project with all of the examples shown in this post. You can find it right here.

There’s also an option to set the “automatically adjusts font” flag in Interface Builder.

In Interface Builder, the Dynamic Type option to automatically adjust fonts applies only to text styles or scaled fonts returned by UIFontMetrics. It has no effect on custom fonts set in Interface Builder.

From Apple’s Developer Documentation.

Notice how we didn’t set a custom font on the label but instead relied on the system font. There isn’t much that is required to support the built-in system scaling.

As we saw above in the default content category size, the text styles are scaled at different sizes. In some cases you may want to provide font sizes that aren’t listed. How can we support Dynamic Type if we want to use a custom font and size in our app?

Custom Font Scaling with UIFontMetrics

So, how can we guarantee our font size will be met with the Dynamic Type requirements? In order to observe changes, we will need to subscribe to UIContentSizeCategory.didChangeNotification.

This sounds simple enough, but what if we have multiple screens to observe? It doesn’t seem very optimal to register for the same notification on each view controller. If your app is primarily navigation controller-based, a way around this would be to subclass UINavigationController. We would simply iterate through the array of child view controllers (and their children) to set the preferredContentSizeCategory and override the trait collection to scale our custom font. Our notification to observe dynamic font size changes would look like this:

NotificationCenter.default.addObserver(self, selector: #selector(overrideChildrenContentSizeCategories), name: UIContentSizeCategory.didChangeNotification, object: nil)

The key here is to override the view controller’s trait collection with the correct UIContentSizeCategory whether it’s user selected or the current preferredContentSizeCategory. That looks something like this:

// Overrides the font cateogry to be used.
override func addChild(_ childController: UIViewController) {
    super.addChild(childController)
    
    overrideContentSizeCategory(childController)
}

func overrideContentSizeCategory(_ child: UIViewController) {
        
   // Local storage
   let preferences = Preferences()
        
   let contentSizeCategory: UIContentSizeCategory
        
   // Whether to use the user-selected content size category or the system one.
   if preferences.shouldUseUserSelectedContentSizeCategory, let userSelectedContentSizeCategory = preferences.userSelectedContentSizeCategory {
       contentSizeCategory = userSelectedContentSizeCategory
   } else {
       contentSizeCategory = UITraitCollection.current.preferredContentSizeCategory
   }
        
   // The setting to scale the font.
   let traitCollection = UITraitCollection(preferredContentSizeCategory: contentSizeCategory)
   setOverrideTraitCollection(traitCollection, forChild: child)
}

All that’s left is to apply the font as a type of UIFontMetrics.

If you use a custom font in your app and want to let the user control the text size, you must create a scaled instance of the font in your source code. Call scaledFont(for:), passing in a reference to the custom font that’s at a point size suitable for use with large. This is the default value for the Dynamic Type setting. You can use this call on the default font metrics, or you can specify a text style, such as headline.

From Apple’s Developer Documentation.

Applying this to our label above would look like this:

if let customFont = UIFont(name: "Roboto-Italic", size: 17) {
    textLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: customFont)
}

Let’s see what this looks like for a standard UILabel.

A gif that shows text sizes scaling up and down.

This is all that’s needed to scale custom fonts with Dynamic Type. Sometimes in content-driven apps there is a need for web technologies for complex layouts / styles. Let’s see if we can support Dynamic Type and web-driven content via WKWebView.

Dynamic Type and WKWebView

Sometimes apps need to display HTML content in a WKWebView. What do we need to do to make sure the typography in the web content can scale with Dynamic Type? Let’s add an HTML and CSS stylesheet snippet like this:

<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
        <link rel="stylesheet" href="stylesheet.css">
    </head>
    <h4> Hi, we’re Lickability: a software studio in NYC that builds apps for clients like Jet, The Atlantic, Meetup, and more.</h4>
    <body>
    <p>We combine expert engineering with a touch of magic. The result? Beautiful, high-quality apps our clients (and their customers) love. ❤️</p>
    </body>
</html>
@font-face {
    font-family: 'RobotoMono';
    src: url('RobotoMono-Regular.ttf');
    font-style: normal;
    font-weight: normal;
}

html {
    font-family: 'RobotoMono';
}

Unfortunately, this doesn’t work unless you specify an Apple system font like -apple-system-body. But we want the web view styled with the one in the HTML file. If we take what we learned above and apply that to our HTML styling it should look something like this:

func reloadWebView() {

    // Setting a placeholder font since the font is loaded through CSS. The font now relies on the system to scale our custom font after calling `scaledFont`.
    let scaledFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .preferredFont(forTextStyle: .body), compatibleWith: traitCollection)

    guard let localHTMLURL = Bundle.main.url(forResource: "example", withExtension: "html"),
         let htmlString = try? String(contentsOf: localHTMLURL) else {
         return
    }
            
    // A quick way to style html without modifying the css stylesheet.
    loadHTML(withFont: scaledFont, htmlString: htmlString)
}

private func loadHTML(withFont font: UIFont, htmlString: String) {
        
    let fontSetting = "<span style=\"font-size: \(font.pointSize)\"</span>"
    webView.loadHTMLString(fontSetting + htmlString, baseURL: Bundle.main.bundleURL)
}

Since we’re using a web view we need to override traitCollectionDidChange(_:) since the UIContentSizeCategory has changed and call reloadWebView() there.

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
   super.traitCollectionDidChange(previousTraitCollection)
   reloadWebView()
}

We’ll see this example in action in the next section when we learn about environment overrides.

Environment Overrides

Luckily we can debug this without leaving the simulator. You can find that in Xcode via the Debug → View Debugging → Configure Environment Overrides. There you will see a switch to toggle text and a Dynamic Type slider to adjust the font. This is what that looks like when running the example app.

A gif showing how to scale Dynamic Type text sizes in the Environment Overrides menu in the Xcode simulator.

Conclusion

We observed how Dynamic Type works for native and web-based UI components. We also learned how UIFontMetrics offloads some of the work needed to scale custom fonts. Maybe one day we’ll see built-in custom font support for web views, but for now, UIFontMetrics is a viable solution for font scaling.