How to build your own resizable font setup with UIFontMetrics

UIFontMetrics was introduced in iOS 11 to allow scaling any font in the same way the default SFDisplay and SFUI fonts are scaled. This means that there's not really much of an excuse for not supporting fonts sized to match the user's preferred sizing.

It's well worth reading the excellent reference Apple provides to get a better understanding of the way UIFontMetrics and UIFontTextStyle are intended to work together.

I've also created a sample project to help test out your custom font stack, which is available on Github.

The quickest possible usage:

Get an automatically resizing UIFont with your custom font chosen:

func genericResizableFont() -> UIFont {
    // Where size is your own, preferred "Default" font size for UIContentSizeCategoryLarge
    let customFont = UIFont(name: "Helvetica", size: 17.0)!

    return UIFontMetrics.default.scaledFont(for: customFont)
}

Or to scale based on the style of text:

func genericResizableFont(for textStyle: UIFontTextStyle) -> UIFont {
    // Where size is your own, preferred "Default" font size for UIContentSizeCategoryLarge
    let customFont = UIFont(name: "Helvetica", size: 17.0)!

    return UIFontMetrics(forTextStyle: textStyle).scaledFont(for: customFont)
}

Important Notes:

  • The point size used should always be the "default" size, as the system will scale from there (so don't increase the point size provided based on the UIContentSizeCategory)

  • The scaling is not 100% consistent between fonts, and you should test the scaling sizes (see later on for a nice tester you can download)

A more involved, turn-key approach

Apple encourages the use of their existing UIFontTextStyle classes for sizing text, so it might be useful to use that to apply a style to a label directly. This may not work in all settings, especially as your font stack gets more complicated and/or involved AttributedStrings, but as a starter I'm providing some helpful extensions that you can use as a reference for building your own font stack.

Step One: Map out your chosen font weights and sizes

For the sake of keeping things all in one file, I've generated a set of fileprivate constants that can be packeded up in an extension on UIFontTextStyle to make iteration go a bit faster.

Font names can be found via methods on UIFont which are documented here.

fileprivate let _largeTitle: (String, CGFloat)   = ( "AvenirNext-Heavy", 34.0 )

fileprivate let _title1: (String, CGFloat)       = ( "AvenirNext-DemiBold", 28.0 )
fileprivate let _title2: (String, CGFloat)       = ( "AvenirNext-Medium", 22.0 )
fileprivate let _title3: (String, CGFloat)       = ( "AvenirNext-Regular", 20.0 )

fileprivate let _headline: (String, CGFloat)     = ( "AvenirNext-DemiBold", 17.0 )
fileprivate let _subheadline: (String, CGFloat)  = ( "AvenirNext-UltraLight", 17.0 )

fileprivate let _body: (String, CGFloat)         = ( "AvenirNext-Regular", 17.0 )
fileprivate let _callout: (String, CGFloat)      = ( "AvenirNext-DemiBold", 16.0 )

fileprivate let _footnote: (String, CGFloat)     = ( "AvenirNext-Regular", 15.0 )

fileprivate let _caption1: (String, CGFloat)     = ( "AvenirNext-Medium", 13.0 )
fileprivate let _caption2: (String, CGFloat)     = ( "AvenirNext-Regular", 11.0 )

Step 2: Build your extension to UIFontTextStyle

// UIFontTextStyle+CustomFont.swift

extension UIFontTextStyle {

    /**
     The UIFont metrics for a given font style, useful if you need to do content sizing as well.
     */
    var fontMetrics: UIFontMetrics {
        return UIFontMetrics(forTextStyle: self)
    }

    /**
     The scaled custom UIFont for a given text style, optionally scaled per the trait collection
     */
    func scaledCustomFont(for traitCollection: UITraitCollection? = nil) -> UIFont {
        let (name, size) = UIFontTextStyle.sizeMap[self]!
        let customFont = UIFont(name: name, size: size)!

        if let traitCollection = traitCollection {
            if let maximumPointSize = maximumPointSize {
                return fontMetrics.scaledFont(for: customFont, 
                                 maximumPointSize: maximumPointSize,
                                   compatibleWith: traitCollection)
            }
            return fontMetrics.scaledFont(for: customFont, compatibleWith: traitCollection)
        }

        if let maximumPointSize = maximumPointSize {
            return fontMetrics.scaledFont(for: customFont, maximumPointSize: maximumPointSize)
        }

        return fontMetrics.scaledFont(for: customFont)
    }

    /**
     The un-scaled custom UIFont for a given text style
     */
    fileprivate var customFont: UIFont {
        let (name, size) = UIFontTextStyle.sizeMap[self]!
        return UIFont(name: name, size: size)!
    }

    /**
     (Optionally) Provide the maximum point size by text style
     */
    fileprivate var maximumPointSize: CGFloat? {
        switch self {
        case .largeTitle: return 44.0
        default: return nil
        }
    }

    fileprivate static let sizeMap: [ UIFontTextStyle: (String, CGFloat) ] = [
        .largeTitle:  _largeTitle,
        .title1:      _title1,
        .title2:      _title2,
        .title3:      _title3,
        .headline:    _headline,
        .subheadline: _subheadline,
        .body:        _body,
        .callout:     _callout,
        .footnote:    _footnote,
        .caption1:    _caption1,
        .caption2:    _caption2,
        ]

}

Step 3: Build a subclass of UILabel that uses the style methods

// TextStyleLabel.swift


/*
 A subclass of UILabel intended to by provided font sizing only via
 `textStyle`.

 This UILabel will default to resizing based on the content size category.
 */
public class TextStyleLabel: UILabel {

    public var textStyle: UIFontTextStyle = .body {
        didSet {
            font = textStyle.scaledCustomFont()
            adjustsFontForContentSizeCategory = true
        }
    }

}

Aside: Finding the names of your fonts

It's not always the case that your font is named in the way you might expect, and if you get the name wrong your app is going to crash (yikes!) or your fonts won't show up as expected (also yikes!).

Here's how you can quickly dump the known font names to find the names you want to use:

// UIFont+Debug.swift

extension UIFont {

    #if DEBUG

    static func listAllFonts() {
        // First grab all known family names
        UIFont.familyNames.forEach { family in
            /** For each family, we'll print the family name, then a
                1-tab indented list of each name within the family:

                For example:

                Futura
                    Futura-CondensedExtraBold
                    Futura-Medium
                    Futura-Bold
                    Futura-CondensedMedium
                    Futura-MediumItalic
            */

            print(family)
            print("\t\(UIFont.fontNames(forFamilyName: family).joined(separator: "\n\t"))")
        }
    }

    #endif

}

Aside: Adding in additional text styles

Since UIFontTextStyle is just a RawRepresentable you can add your own variations if you really need to (though I'd suggest keeping this pretty light).

// UIFontTextStyle+CustomFont.swift
extension UIFontTextStyle {

    public static let ultraLargeTitle: UIFontTextStyle = .init(rawValue: "TitleUltraLarge")

}

Aside: Other helpful extensions for UIFontTextStyle

// UIFontTextStyle+CustomFont.swift
extension UIFontTextStyle {

    static let allStyles: [UIFontTextStyle] = [
        .largeTitle,
        .title1,
        .title2,
        .title3,
        .headline,
        .subheadline,
        .body,
        .callout,
        .footnote,
        .caption1,
        .caption2
    ]

    var debugDescription: String {
        // Drop the `UICTFontTextStyle` prefix, which is 17 characters. Hooray, magic numbers :-D
        return String(self.rawValue.dropFirst(17))
    }

}

Get the full project + font testing view:

The font testing view and all related sample code is available on Github. If you find it useful, please consider starring it and/or sharing it on Twitter.

Share this on Twitter

Star


There's more to read!

Did this help solve your problem? If so, consider letting us know on Twitter. You should follow us for all the latest articles and updates:


Have you seen our latest project on Github?.

Sign up for our newsletter

Get an email when new content comes out! Emails are not sold and are used only for announcing updates to the site. Expect ~1-2 emails per month. Unsubscribe any time.