Since iOS 7, Dynamic Type has allowed users to choose a prefered font size for their phone. At Airbnb, we try to build an app that our entire community can use — since Dynamic Type is a critical accessibility feature, we knew supporting it would make more people able to effectively use our app, some of them probably for the first time. To validate the importance of this feature, we examined the data and saw as much as 30% of people using our app had a preferred font size that was not the default. This usage is not skewed towards particular sizes, but evenly spread across larger and smaller than default.
30% of people using the app had a preferred font size that was not the default.
It turns out, supporting this preference creates a consistent experience across the OS that users will notice. Experimenting with Dynamic Type on individual features in the Airbnb app resulted in a significant increase in engagement, helping move our bottom line metrics. If you spend the time to support Dynamic Type in your app too, users will surely thank you for it!
Why is it important?
Going beyond the metrics, supporting Dynamic Type holds your UI components to a higher level of quality. Layout will need to be robust enough to handle a wide range of preferences, similar to variations created by localization and device screen size. Since much of our development time is spent on single devices and languages, bugs only reproducible in certain configurations will too often slip through. Fortunately, many of these are now being caught during Dynamic Type testing. If you already support varying screen sizes through UITraitCollection and translations with various length strings, there’s a good chance you’ve done most of the work to support Dynamic Type.
The majority of bugs we encountered when large font sizes were used had to do with text not fitting their containers. To resolve these, we created a few design recommendations. First, widths and heights became flexible, allowing text to expand to multiple lines. In many cases this should have already been done since some languages can be much longer than the English words we include in design mocks.
Second, we had to make sure fonts scale the correct amount. This is done by assigning every text a corresponding UIFont.TextStyle. Using a larger TextStyle indicates your font is already big, so it doesn’t need to increase in size as much.
Third, we had to fix some labels that were changing size even though they shouldn’t be eligible to scale. Our recommendation is everything part of the scrollable area on the screen should scale, and everything else should be left static. However, anyone with large Dynamic Type enabled still needs a way to view text in smaller containers such as tab bars. If you use all standard UIKit elements this is handled by the Large Content Viewer:
We filed a bug report with Apple, requesting a new API for presenting these popups from custom views. This capability has since been included in the iOS 13 beta, so you’ll be able to see it in the Airbnb app soon.
iOS provides mostly automatic Dynamic Type APIs for system fonts, but the Airbnb app uses a custom typeface, Cereal. To support Dynamic Type, we rely on UIFontMetrics. This class handles scaling our font size, line height, and tracking.
Each of these attributes exist in two forms:
- The unscaled units which are shown to users with the default font size, and are the values we set when creating font attributes.
- The scaled units which fit the
preferredContentSizeCategoryand are what we read at runtime.
Internally, features will request a font using attributes expressed as unscaled units, and will receive an object containing various functions we use to display text which always returns values in scaled units. This ensures any calculation, such as bounding box of text, will use the scaled units. Some of the most common bugs we saw were caused by using unscaled units for layout calculations instead of scaled units.
There are two ways to use UIFontMetrics to convert from unscaled to scaled units.
func scaledFont(for font: UIFont, compatibleWith traitCollection: UITraitCollection?) -> UIFont
func scaledValue(for value: CGFloat, compatibleWith traitCollection: UITraitCollection?) -> CGFloat
There are subtle differences in these approaches. Consider the following examples, each using a different UIFontMetrics method:
Depending on the device you run on, we observed results like this:
The results aren’t quite consistent, but since we customize line height with NSParagraphStyle we need to use the CGFloat scale function. The UIFont with unscaled point size is scaled directly to get an adjusted UIFont. Here’s a full example to scale an NSAttributedString:
The last step to fully supporting Dynamic Type is to encourage validation across all features, including ones in development. We know developer time is limited, so we automated support for Dynamic Type as much as possible. Happo, the tool we use for UI regression detection, already snapshots existing components. We added an additional step to render with the accessibilityExtraExtraExtraLarge size.
There are no APIs available to programmatically change the simulator’s Dynamic Type settings, so avoid using
UIApplication.shared.preferredContentSize. A more testable approach is to query the trait collection of a UIView. In the UIWindow for our Happo Tests, the traitCollection is configured to include a custom content size. The end result is snapshots like these:
With snapshots generated on every code change to the iOS app, developers have a hassle-free way to know new features support Dynamic Type, and easily detect regressions.