Dynamic Type: Scaling Custom Fonts
Hello, Iām Kevin, iOS developer at Immoweb. This article will be one of a series about supporting Dynamic Type.
But before reading further, you might askā¦
What is Dynamic Type? Why should I support it in my app?
Letās first answer those questions!
If you know the answers already, you can skip them.
What is Dynamic Type?
Dynamic Type is a feature provided by Apple.
It allows you to scale your appās interface (texts and images) and adapt your layouts based on the userās preference.
A user can define a preferred text size to use across the system in Settings ā Accessibility ā Display & Text Size.
iOS ecosystem and every app should adapt according to the chosen size.
Why supporting Dynamic Type?
Because itās the right thing to do, period. Thereās even a saying: āDo what is right, not what is easy.ā
Need some other good reason? Well, itās a win-win. It will improve the user experience of your app, and you will benefit from it, because it impacts a lot of users.
Some statistics
The World Health Organization reported that about 15% of worldwide population lives with some form of disability.
If we look at statistics from specific countries, we get 26% in the United States and 18% in France.
But this gathers all types of disabilities. What about visual disabilities? How many people are using the Dynamic Type functionality in iOS?
In the Immoweb iOS app, it has been over a year since we started recording statistics about which accessibility features are used by our users.
Immoweb is one of the biggest Belgian apps (and is mostly used in Belgium)
If we look specifically at users using the Dynamic Type functionality, we can clearly see it gathers 29% of our audience. We need to make their experience with the app a great one.
Adapting to bigger sizes, for accessibility, allows us also support people who wants smaller sizes (and there are almost as many).
First approach
If you are using custom fonts in your app and want them to scale properly, this is the basic approach:
let customFont = UIFont(name: "CustomFont-Light", size: 16)!
label.font = UIFontMetrics.default.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true
- You use
UIFontMetrics.default.scaledFont(for:)
method to get a version of your font scaled to the current Dynamic Type size. - You set
adjustsFontForContentSizeCategory
totrue
to allow the font to update when the size category changes.
Support for iOS 10 and earlier
For those who still need to support iOS 10 and earlier version, UIFontMetrics is not available. I redirect you to this solution.
Supporting a custom design system
In our app, we use different text styles which could be mapped to UIFont.TextStyle
s.
But as it often happens, in our design system, we are using slightly different styles and we donāt want to constraint ourselves to Apple text styles.
Hereās a comparison of Apple text styles and those from Immoweb design system:
You can see we are using more styles than Apple and depending on the style, we use one of two fonts: Montserrat or PT Sans.
Use our custom text styles
So we decided to go with our own custom text styles:
struct TextStyle {
let size: CGFloat
let emphasis: Emphasis
let name: Name
init(size: CGFloat, emphasis: Emphasis = .none, name: Name = .ptSans) {
self.size = size
self.emphasis = emphasis
self.name = name
}
}
extension TextStyle {
typealias Name = String
enum Emphasis {
case none
case bold
case italic
var symbolicTraits: UIFontDescriptor.SymbolicTraits? {
switch self {
case .none:
return nil
case .bold:
return .traitBold
case .italic:
return .traitItalic
}
}
}
}
extension TextStyle.Name {
static let montserrat: TextStyle.Name = "Montserrat-Regular"
static let ptSans: TextStyle.Name = "PTSans-Regular"
}
So far:
- We have created a
TextStyle
struct that will be used to define each of our text styles. - We created an init defaulting to no emphasis and using our main font, PT Sans.
- We created an
Emphasis
enum to specify if the text style should be in bold, italic or stay regular. We chose to not use directlyUIFontDescriptor.SymbolicTraits
and all its values because we donāt need them (so far). So we created this enum with only the cases we need, to be more clean.
Defining our text styles
Now we can add our styles like so:
extension TextStyle {
// MARK: - Headings
static let h1 = TextStyle(size: 24, emphasis: .bold, name: .montserrat)
static let h2 = TextStyle(size: 20, emphasis: .bold, name: .montserrat)
static let h3 = TextStyle(size: 18, emphasis: .bold, name: .montserrat)
static let h4 = TextStyle(size: 16, emphasis: .bold, name: .montserrat)
static let h5 = TextStyle(size: 14, emphasis: .bold, name: .montserrat)
static let h6 = TextStyle(size: 13, emphasis: .bold, name: .montserrat)
// MARK: - Text buttons
static func buttonLarge(emphasis: Emphasis) -> TextStyle {
TextStyle(size: 16, emphasis: emphasis)
}
static func buttonSmall(emphasis: Emphasis) -> TextStyle {
TextStyle(size: 14, emphasis: emphasis)
}
// MARK: - Texts
static let text18 = TextStyle(size: 18)
static let text16 = TextStyle(size: 16)
static let text13 = TextStyle(size: 13)
static let text14 = TextStyle(size: 14)
// MARK: - Text highlights
static let textHighlight24 = TextStyle(size: 24, emphasis: .bold)
static let textHighlight20 = TextStyle(size: 20, emphasis: .bold)
}
We decided to make TextStyle a struct and not an enum so we can easily extend it to create new text styles if needed. And itās usually the case. Youāll end up with a few exceptions for certain specific UIās.
Now letās see how weāll use those styles weāve just createdā¦
Extend UIFont
We are going to create fonts with our new TextStyle
by extending UIFont
class with 2 static functions:
extension UIFont {
/// Returns a font matching `style`
static func font(style: <ModuleName>.TextStyle) -> UIFont {
var descriptor = UIFontDescriptor(name: style.name, size: style.size)
if let symbolicTraits = style.emphasis.symbolicTraits {
descriptor = descriptor.withSymbolicTraits(symbolicTraits)!
}
return UIFont(descriptor: descriptor, size: style.size)
}
/// Returns a font matching `style` scaled to the current Content Size Category.
static func scaledFont(style: <ModuleName>.TextStyle) -> UIFont {
UIFontMetrics.default.scaledFont(for: font(style: style))
}
}
Here we use the UIFontDescription
class to be able to specify symbolicTraits
.
We can now use this to get the font from one of our TextStyle
:
label.font = .scaledFont(style: .h1)
label.adjustsFontForContentSizeCategory = true
It is clean š
We can even call UIFont.font(style: .h1)
if we need a fixed sized font (like for tab bar and navigation bar texts).
Now we can see that thereās still something we could improve:
Each time we use label.font = UIFont.scaledFont(style: ā¦)
we have to also add the statement label.adjustsFontForContentSizeCategory = true
which is a bit redundant.
What we would do, ideally, is to have an API that looks like this:
label.apply(textStyle: ā¦)
And it would, by default, use the font scaled to the current content size category and adapt to content size category changes.
Letās see how we can implement thatā¦
Create a common protocol
We want to add a apply(textStyle:)
method to UILabel
, UITextField
, UITextView
and UIButton
. For that weāll create a common protocol. Letās call it TextStyleAdjustable
:
protocol TextStyleAdjustable: UIContentSizeCategoryAdjusting {
func setFont(_ font: UIFont)
@discardableResult
func apply(textStyle: TextStyle) -> Self
}
extension TextStyleAdjustable {
@discardableResult
func apply(textStyle: TextStyle) -> Self {
setFont(.scaledFont(style: textStyle))
adjustsFontForContentSizeCategory = true
return self
}
}
Our protocol inherits from UIContentSizeCategoryAdjusting
so we can have access to the variable adjustsFontForContentSizeCategory
.
Now we can implement this protocol:
extension UILabel: TextStyleAdjustable {
func setFont(_ font: UIFont) {
self.font = font
}
}
extension UITextView: TextStyleAdjustable {
func setFont(_ font: UIFont) {
self.font = font
}
}
extension UITextField: TextStyleAdjustable {
func setFont(_ font: UIFont) {
self.font = font
}
}
extension UIButton: TextStyleAdjustable {
func setFont(_ font: UIFont) {
titleLabel?.font = font
}
public var adjustsFontForContentSizeCategory: Bool {
get {
titleLabel?.adjustsFontForContentSizeCategory ?? false
}
set {
titleLabel?.adjustsFontForContentSizeCategory = newValue
}
}
}
Note that UILabel
, UITextField
, UITextView
already implement UIContentSizeCategoryAdjusting
protocol so we donāt need to implement adjustsFontForContentSizeCategory
variable for them, but we do for UIButton
.
We can use our new API:
label.apply(textStyle: .h1)
textField.apply(textStyle: .text16)
textView.apply(textStyle: .text16)
button.apply(textStyle: .buttonLarge(emphasis: .bold))
And all texts in our app will be correctly sized and adapt automatically, all of that with custom fonts š
Add a SwiftLint check
If you are using SwiftLint I recommend adding a custom rule to make sure we donāt use unwanted APIās by mistake, such as:
UIFont.preferredFont(forTextStyle:)
UIFont.systemFont(ofSize:)
UIFont.boldSystemFont(ofSize:)
- etc.
# https://github.com/realm/SwiftLint#defining-custom-rules
custom_rules:
use_scaled_fonts:
name: "Use scaled fonts: UIFont.scaledFont(style:)"
regex: '\.(?!scaled)[a-zA-Z]*[fF]ont\('
severity: error
This way weāll avoid using the wrong APIs by mistake. You can change severity: error
to warning
to start if needed.
You can still disable the rule in specific cases by adding //swiftlint:disable:next use_scaled_fonts
on the line before the call to one of the prohibited APIās we listed above. An example of a case we would do that is when setting a font for the tab bar, which should never scale.
Final code
You can have the final code here.
Conclusion
We saw that the Dynamic Type feature is used by many users (29% of Immoweb iOS audience) and it is important to support it in our apps.
As it is usually the case, our design systems include more text styles than Apple is offering by default with UIFont.TextStyle
, for this reason we created our own TextStyle
struct to support them.
We want scalable texts to be the default behaviour in our apps, so we created APIās to enforce that: UIFont.scaledFont(style:)
and <textField/textView/label/button>.apply(textStyle:)
.
To go further, in the next articles weāll see how we can update our layout, scale our images dynamically to the Dynamic Type and moreā¦
I hope you liked this article!
See you soon š
Resources
- Scaling Fonts Automatically ā Apple Developer Documentation
- Building Apps with Dynamic Type ā WWDC Video
- Dynamic Type Sizes ā Human Interface Guidelines
- Adding a Custom Font to Your App ā Apple Developer Documentation