Overriding the iOS Dynamic Type font family

Written by Emil Loer on Oct 31, 2016 |

Recent versions of iOS provide Dynamic Type: a fantastic system for adding accessible typography to your app. By adopting Dynamic Type the user can define a system-wide font size which will then also be reflected in your own app. Dynamic Type even provides a couple of predefined text styles - such as titles, captions, and footnotes - which you can use to modify the typographical salience of your content.

This marketing blurb makes it sound awesome but also makes you wonder why not all apps support it. Well, it turns out that Dynamic Type currently has a big drawback. In order to make your own app stand out from the crowd you want to use a custom fancy-pants font, but it turns out that Dynamic Type won't allow this. So you have to make a decision... or do you?

Modifying Font Descriptors

Using the UIFontDescriptor class we can get some kind of specification for a given font. The descriptor codifies information such as the font family, font name, weight, style, and a lot of other metadata we don't necessarily care about. Getting a descriptor from a font is simple: just take it from the fontDescriptor property of a UIFont instance. To convert the descriptor back to an actual font we just pass it as an argument to one of the UIFont constructors.

Now, the goal here is that we want to take a font instance created by Dynamic Type and convert it to a font that has our own custom family name but keeps al the other properties intact. This is not easy because if we just change the font family name of a descriptor the font weight will be reset. Furthermore, if we take the font descriptor and we just start modifying things we will get a font descriptor that still contains some properties that on some occasions override our custom font family.

This means we have to be a bit more clever. First we extract the necessary font traits and then we create a new font descriptor from nothing and just specify the exact traits we are interested in. This results in the following snippet of code:

// Get a font from Dynamic Type
var font = UIFont.preferredFont(forTextStyle: UIFontTextStyleHeadline)

// Our overridden font family name
let newFamilyName = "Avenir Next"

// Extract the weight
let weight = (font.fontDescriptor.object(forKey: UIFontDescriptorTraitsAttribute)
    as! NSDictionary)[UIFontWeightTrait]!

// Create a new font traits dictionary
let attributes = [
    UIFontDescriptorTraitsAttribute: [
        UIFontWeightTrait: weight
    ]
]

// Create a new font descriptor
let descriptor = UIFontDescriptor(name: font.fontName, size: font.pointSize)
    .withFamily(newFamilyName)
    .addingAttributes(attributes)

// Find a font that matches the descriptor
font = UIFont(descriptor: descriptor, size: font.pointSize)

Global font family overrides with appearance proxy

The method given above is already really powerful but still requires you to manually set fonts everywhere. We can change the code above to a computed property that can be set globally with the UIAppearance proxy protocol, e.g. on a label. This is done as follows:

/// Extension allowing global UILabel font family overriding via appearance proxy
extension UILabel {
    var fontFamily: String {
        get {
            // Extract the font family from the current descriptor. This is not really
            // necessary but provides a sane value for the required getter.
            return font.fontDescriptor.object(forKey: UIFontDescriptorFamilyAttribute)
                as! String
        }

        set {
            // Extract the weight
            let weight = (font.fontDescriptor.object(forKey: UIFontDescriptorTraitsAttribute)
                as! NSDictionary)[UIFontWeightTrait]!

            // Create a new font traits dictionary
            let attributes = [
                UIFontDescriptorTraitsAttribute: [
                    UIFontWeightTrait: weight
                ]
            ]

            // Create a new font descriptor
            let descriptor = UIFontDescriptor(name: font.fontName, size: font.pointSize)
                .withFamily(newValue)
                .addingAttributes(attributes)

            // Find and set a font that matches the descriptor
            font = UIFont(descriptor: descriptor, size: font.pointSize)
        }
    }
}

Now using only a single line of code we can automatically override the font family of all UILabel instances in our app, while maintaining size and font style from Dynamic Type. Just add this line to your application delegate class:

UILabel.appearance().fontFamily = "Avenir Next"

With this line in place you can configure your labels to use the predefined Dynamic Type style classes for maximum accessibility and still enjoy the typographical fanciness of a custom font!

If you've enjoyed this post please follow me on Twitter or Facebook. I'd really appreciate it!