Dynamic Type at any font weight

I recently realized that I never give Dynamic Type any love. It's such an important part of iOS and accessibility, and yet I never give it the time that it deserves. I even use it myself (my text size is set to the smallest because I like to fit more on screen).

So in the latest app I started building, I really took the time to make sure that I was supporting Dynamic Type. The only issue I found while using it was that you are locked into Apple's Dynamic Type text styles unless you want to do some extra heavy lifting.

Getting started with Dynamic Type

If you're here just for font weight, you can skip right to it

First thing's first. Let's take a look at a basic example of Dynamic Type in code.

let titleLabel = UILabel()
titleLabel.font = UIFont.preferredFont(forTextStyle: .title1)
titleLabel.adjustsFontForContentSizeCategory = true

let bodyLabel = UILabel()
bodyLabel.font = UIFont.preferredFont(forTextStyle: .body)
bodyLabel.adjustsFontForContentSizeCategory = true

As you can see, setting Dynamic Type is pretty easy. Apple provides a variety of text styles for Dynamic Type in the UIFont.TextStyle enum

All this is great, but what about constraints? How do I make my layout adapt to accessibility changes?

Luckily Apple has also created ways to constrain your labels so that the spacing between your labels will adapt with the font changes.

Let's say we have two labels in a view, and we want to constrain the labels vertically:

let guide = contentView.layoutMarginsGuide

titleLabel.firstBaselineAnchor
    .constraintEqualToSystemSpacingBelow(guide.topAnchor, multiplier: 1)
    .isActive = true

guide.bottomAnchor
    .constraintEqualToSystemSpacingBelow(bodyLabel.lastBaselineAnchor, multiplier: 1)
    .isActive = true

bodyLabel.firstBaselineAnchor
    .constraintEqualToSystemSpacingBelow(titleLabel.lastBaselineAnchor, multiplier: 1)
    .isActive = true

What's important to note here is that we are constraining the label baselines. Baselines are different than edges of the label. Whenever constraints or stackviews are baseline relative, it means they will align to the baseline of the text. For more typographical concepts, visit Apple's documentation.

No love for font weight?

For some strange reason, Apple doesn't give you built in functionality to use text styles at any font weight. Even more weird is that they have instances where they have used different font weights in their own apps.

Apple suggests that we use UIFontMetrics when creating our own custom font:

guard let font = UIFont(name: "CustomFont-Light", size: UIFont.labelFontSize) else {
    fatalError("Failed to load the font.")
}
label.font = UIFontMetrics.default.scaledFont(for: font)
label.adjustsFontForContentSizeCategory = true

This creates a custom font and uses the UIFontMetrics.default text style. If we wanted to use a specific text style (headline for example), we would just replace the line with the following:

let font = UIFontMetrics.headline.scaledFont(for: customFont)

But what if we just want to use the built in font, and change the font weight? How would we get that font to begin with?

let font = UIFont.systemFont(ofSize: 17, weight: .bold)

This gives us a bold font, but we had to specify the size. So all that's left to find is the font size given a Dynamic Type text style. We can do this using UIFontDescriptor:

let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .headline)
let size = desc.pointSize

Alright, now let's wrap it all together into a handy extension:

extension UIFont {
    static func preferredFont(for style: TextStyle, weight: Weight) -> UIFont {
        let metrics = UIFontMetrics(forTextStyle: style)
        let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style)
        let font = UIFont.systemFont(ofSize: desc.pointSize, weight: weight)
        return metrics.scaledFont(for: font)
    }
}

Now you can create system fonts for any text style and any font weight. Not it's as simple as this:

let label = UILabel()
label.font = UIFont.preferredFont(for: .title1, weight: .bold)
label.adjustsFontForContentSizeCategory = true