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 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 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
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)
}