Guide to Supporting Dynamic Type

Link to Guide to Supporting Dynamic Type copied to clipboard

iOS applications must scale content to ensure no loss of information or functionality for users requiring larger font sizes to conform to WCAG 1.4.4 Resize Text. Information should be made available to everyone. This guide will cover recommended ways to support Dynamic Type to ensure the user experience is suited for everyone.

Dynamic Type is an accessibility feature within iOS that allows font sizes to be scaled device-wide based on the user's preference. Adjusting Dynamic Type settings can be found under Accessibility Settings on your device. Read more from Apple on Dynamic Type here.

Prepare Your View

Before implementing Dynamic Type, your application's views must be ready. Read the below considerations before implementation.

Ensure Content Can Scroll

A typical design paradigm within iOS applications is to have content available without scrolling. While it does allow for digestible content, it's important to highlight that it shouldn't scroll under the default settings, not that it can't scroll. Developers should consistently implement a UIScrollView or ScrollView on any screen with content. With Dynamic Type, larger font sizes may knock a substantial amount of content off-screen. If this were to happen without a scrollable view, any information off-screen would not be available to the user.

Ensure Content Can Expand

While working with constraints, keep in mind that parent views will also need to expand to accommodate larger font sizes in text elements. Utilize the contentHuggingPriority, contentCompressionResistancePriority, greaterThanOrEqualTo, and lessThanOrEqualTo to allow your views to expand as needed. Limit setting a constant height and width for a view containing content.

Ensure Usability is Not Affected

Improper constraints may rearrange important views and controls, negatively impacting usability. Test against various font sizes to ensure content views display as expected.

Ensure Text Controls Support Dynamic Type

Any control that includes text can support Dynamic Type. Verify that each control can expand to accommodate various font sizes without pushing content off-screen.

Set Number of Lines

Any text with a chance of overflow should have the numberOfLines property set to 0. This property will allow the view to expand for any number of needed lines. For a UIButton, set the numberOfLines property to 0 on its titleLabel; this will allow the control to expand vertically.

Without a control expanding for growing text, an ellipsis will replace part or all of the text.

Keep Headings Short and Sweet

Headings are extremely useful for VoiceOver navigation. They should be descriptive enough to provide context but short enough to remain on one line when resizing text.

Follow Apple Guidelines

Apple has created multiple guidelines to help developers support Dynamic Type.

Supporting Dynamic Type

There are currently four ways to support Dynamic Type. Any of the methods below will result in similar behavior for the end-user.

Using Default Fonts

Using iOS's default font is the easiest way to support Dynamic Type; it is the only one supported within storyboards and code.

This article from Apple covers setting up Dynamic Type in a storyboard.

To use default fonts programmatically, follow the steps below.

Set Label Font to a Specific TextStyle

Apple provides TextStyles, a way to categorize text in your application, so each style has visible differences. For example, the TextStyle "Title 1" is going to be larger than the TextStyle "Title 2", and both of those TextStyles will be larger than the TextStyle "body." Using a custom font is covered later in Use Font Metrics with Auto Font Scaling.

To set a label’s font to a certain TextStyle, use thepreferredFont(forTextStyle: ...) function:

label.font = UIFont.preferredFont(forTextStyle: .body)

Since each TextStyle is associated with different font sizes and styles, it will scale differently. More information about each TextStyle can be found in Apple's documentation.

Set adjustsFontForContentSizeCategory

Although font can scale, it does not mean it will automatically scale out-of-the-box. Use adjustsFontForContentSizeCategory on all text elements to support chaning Dynamic Type settings:

label.adjustsFontForContentSizeCategory = true

Using Custom Fonts

Below are three ways to support Dynamic Type with custom fonts.

Font Metrics with Auto Font Scaling

If the custom font supports scaling, UIFontMetrics allows the operating system to do all the work!

label.font = UIFont(name: <fontNameHere>, size: UILabel.defaultFontSize)

With UIFontMetrics, you can attach a custom font with a specific TextStyle. Apple's Design Guidelines, mention using different point sizes for each TextStyle. Rather than manually setting the point size for each Dynamic Type setting (shown later), use the provided TextStyle font sizes and link the custom font with the related TextStyle. For example, a font for all captions within the application can be set with the "caption 1" TextStyle:

guard let font = UIFont(name: <fontNameHere>, size: UIFont.labelFontSize) else { return }
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: font)
label.adjustsFontForContentSizeCategory = true
important

Be sure to set adjustsFontForContentSizeCategory to true so the font can automatically respond to a type size change!

Responding to Dynamic Type Size Notifications and Overriding TraitCollectionDidChange are helpful if a custom font does not scale well, or the point sizes for each TextStyle are not appropriate for the font used.

Responding to Dynamic Type Size Notifications

The original notification observers continue to be supported (including Objective-C code). Create a listener for the notification:

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

Then, define the selector. An example is written below:

func changeTextSize() {

    let newFontSize: CGFloat

    switch self.traitCollection.preferredContentSizeCategory {
            
        case .extraSmall: newFontSize = 14
        case .small: newFontSize = 15
        case .medium: newFontSize = 16
        case .large: newFontSize = 17
        case .extraLarge: newFontSize = 19
        case .extraExtraLarge: newFontSize = 21
        case .extraExtraExtraLarge: newFontSize = 23
            
        case .accessibilityMedium: newFontSize = 28
        case .accessibilityLarge: newFontSize = 33
        case .accessibilityExtraLarge: newFontSize = 40
        case .accessibilityExtraExtraLarge: newFontSize = 47
        case .accessibilityExtraExtraExtraLarge: newFontSize = 53
        default: break
    }

    guard let font = UIFont(name: "Arial", size: UIFont.labelFontSize) else {
        self.font = UIFont.preferredFont(forTextStyle: .body)
        return
    }
        
    self.font = font.withSize(newFontSize)
}

Overriding TraitCollectionDidChange

Overriding TraitCollectionDidChange is similar to responding to the Dynamic Type size notification but without the notification listener.

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

    let newFontSize: CGFloat
        
    switch self.traitCollection.preferredContentSizeCategory {
            
        case .extraSmall: newFontSize = 14
        case .small: newFontSize = 15
        case .medium: newFontSize = 16
        case .large: newFontSize = 17
        case .extraLarge: newFontSize = 19
        case .extraExtraLarge: newFontSize = 21
        case .extraExtraExtraLarge: newFontSize = 23
                
        case .accessibilityMedium: newFontSize = 28
        case .accessibilityLarge: newFontSize = 33
        case .accessibilityExtraLarge: newFontSize = 40
        case .accessibilityExtraExtraLarge: newFontSize = 47
        case .accessibilityExtraExtraExtraLarge: newFontSize = 53
        default: break
    }
        
    guard let font = UIFont(name: "Arial", size: UIFont.labelFontSize) else {
        self.font = UIFont.preferredFont(forTextStyle: .body)
        return
    }

    self.font = font.withSize(newFontSize)
}