Guide to Supporting Dynamic Type
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.
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
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)
}