Using A Custom Font With Dynamic Type

Using a custom font with dynamic type has always been possible but it took some effort to get it to scale for each text style as the user changed the dynamic type size. Apple introduced a new font metrics class in iOS 11 that makes it much less painful.

Dynamic Type

Apple introduced dynamic type back in iOS 7 to give the user a system wide mechanism to change their preferred text size from the system settings.

Preferred text size

To support dynamic type you set labels, text fields or text views to a font returned by the UIFont class method preferredFont(forTextStyle:). The returned font, which uses the Apple San Francisco typeface, has a size and weight adjusted for the users size preference and the intended text style.

For example, to create a label with the body text style:

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

Notes:

This is how the various text styles look at extra small, large and accessibility extra-extra-extra large sizes:

Default font

Note how all of the text styles increase in size with the accessibility size. This is new in iOS 11. When the larger accessibility sizes were first introduced in iOS 7 they only applied to the .body style.

Scaling A Custom Font

Before iOS 11 to support dynamic type with a custom font you needed to decide the font details (font face and size) for each of the ten text styles and then decide how to scale those font choices for each of the twelve content size categories.

Apple publishes the font metrics they use for the San Francisco typeface in the iOS Human Interface Guidelines which acts as useful starting point when deciding how to scale each text style.

For example, the .headline text style uses a Semi-Bold face that is 17 pt at the large content size and 23 pt at the xxxLarge size.

Font Metrics

To make it easier to scale a custom font for dynamic type Apple introduced UIFontMetrics in iOS 11. To use a custom font for a given text style you first get the font metrics for that style and then use it to scale your custom font.

Let’s revisit the example of setting a label to the .body text style but with a custom font. The basic approach is this:

let font = UIFont(name: fontName, size: fontSize)
let fontMetrics = UIFontMetrics(forTextStyle: .body)
label.font = fontMetrics.scaledFont(for: font) 

You create your font with the custom font face and size. Get the font metrics for the .body style and then use scaledFont(for:) to get the font scaled for the preferred text size.

The UIFontMetrics class takes away the need to maintain a table of fonts (typeface and size) for each of the twelve content size categories. You do still need to decide on a font for each style at the default content size. This font size is then scaled by the font metrics when the user changes the content size.

A Style Dictionary

To avoid having font face names and sizes scattered through the code I ended up with a style dictionary that has the face name and size to use for each of the text styles at the .large content size. To make it easy to customize and even change typefaces I keep this style dictionary in a plist file.

Here is how it looks for the Noteworthy typeface which Apple bundles with iOS. It has both a bold and a light face:

Noteworthy.plist

I kept to the font sizes that Apple uses for the .large text size for each style. So, for example, I used a 17 pt Noteworthy-Bold for the .headline and a 17 pt Noteworthy-Light for the .body.

To apply the fonts I wrap the dictionary in a ScaledFont utility class that you initialize with the name of the plist file (without the extension). The font(forTextStyle:) method then returns the scaled font for each text style:

public final class ScaledFont {
  public init(fontName: String)
  public func font(forTextStyle textStyle: UIFontTextStyle) -> UIFont
}

Check the code for the full details but here is the interesting method that looks up the font for a text style and then uses UIFontMetrics to return the scaled font. If the style dictionary does not have an entry for a text style it falls back to the Apple preferred font:

public func font(forTextStyle textStyle: UIFontTextStyle) -> UIFont {
    guard let fontDescription = styleDictionary?[textStyle.rawValue],
        let font = UIFont(name: fontDescription.fontName, size: fontDescription.fontSize) else {
            return UIFont.preferredFont(forTextStyle: textStyle)
    }

    let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
    return fontMetrics.scaledFont(for: font)
}

To use this with the Noteworthy.plist I lazily load it in the view controller:

private let fontName = "Noteworthy"

private lazy var scaledFont: ScaledFont = {
    return ScaledFont(fontName: fontName)
}()

Then when setting the font for a label, I call font(forTextStyle:):

let label = UILabel()
label.font = scaledFont.font(forTextStyle: textStyle)
label.adjustsFontForContentSizeCategory = true

As long as you are scaling the font with UIFontMetrics the adjustsFontForContentSizeCategory property still works so you do not need to worry about updating when the user changes the size. Here is how it looks using the Noteworthy font.

Noteworthy

Note: I am not sure if it is a bug or a “feature” but the .caption2 style seems to scale larger than the .caption1 style even though it uses a smaller point size at the .large size.

Using A Custom Font

You are not restricted to the typefaces included with iOS. This is NotoSans downloaded from google fonts (check the license of any fonts you download if you ship them with your application). It has regular, bold, italic and bold-italic faces. I used the italic for the subheadline and caption styles:

NotoSans

If you are downloading and adding custom font files to your project don’t forget to add them to the target and list them under the “Fonts provided by application” (UIAppFonts) key in the Info.plist:

Info.plist

If you are not sure of the font names to use you can print all available names with this code snippet:

let families = UIFont.familyNames
families.sorted().forEach {
  print("\($0)")
  let names = UIFont.fontNames(forFamilyName: $0)
  print(names)
}

Get The Code

You can get the full code for this post in my CodeExamples GitHub repository:

Further Reading

See the WWDC 2017 session on dynamic type:

Never miss a post!

iOS Size Classes Cheat Sheet

Subscribe and get my free iOS Size Classes Cheat Sheet

Success! Now check your email to confirm your subscription and download your free guide to iOS Size Classes.

There was an error submitting your subscription. Please try again.

Unsubscribe at any time.
No time to watch WWDC videos?

Sign up to get my iOS posts direct to your inbox and I will send you a free PDF of my iOS Size Classes Cheat Sheet.

OK! Check your inbox (or spam folder) for an email to confirm your details and download your free guide to iOS Size Classes.

There was an error submitting your subscription. Please try again.

Unsubscribe at any time.
Archives Categories