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 Text Display here.

Prepare Your View

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

note

For views in SwiftUI, some of the below considerations are provided by default. Review each to ensure your text elements behave as guided.

Ensure Content Can Scroll

A popular platform design paradigm keeps content and elements available in the main view without the need to scroll. While it is great for digestible content and keeping UI minimal, it's important to highlight that these designs shouldn't scroll under the default settings, not that it can't scroll.

To properly support your customers using assistive technology such as larger font sizes, it's important to consistently implement a UIScrollView or ScrollView on any screen with content. Once content is scaled to larger font sizes, a substantial amount of content could be pushed off-screen. If someone using Dynamic Type loads your app, and it doesn't contain a scrollable view, information would not be readable.

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 the 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

SwiftUI Views

In iOS 14 or above, you can support Dynamic Type in two ways.

Using Custom Fonts

When using a custom font, set the .font property to .custom(_:size:relativeTo:) to ensure that fonts will relatively scale to the font style of the text element.

Using Default Fonts

When using a default font by size, text will scale automatically, however, if no font style is set, text elements will not scale according to their style. For example, text that is functioning as a title will scale differently than text that is functioning as body text - as the text gets bigger the title text will always be bigger than the body text.

For the best experience, be sure to specify the font style to match the element's expected behavior. By using a modifier such as: .font(.system(.largeTitle, design: .rounded)), you can expect the text to be the largest text on the page and serve as a proper title.

UIKit Views

When building UIKit views, 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 the preferredFont(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 changing 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)
}