A better way to use Dynamic Type with a custom font in Swift 3/iOS10

Frederik's Winkelsdorf version fo code but tweaked to use two different Font Family (default one and bold) plus sample of usage.

MyFontExtension.swift

import UIKit
extension UIFontDescriptor {
    private struct FontFamily {
        static var preferredFontNameRegular: String = "Montserrat-Regular"
        static var preferredFontNameBold: String = "Montserrat-Bold"
    }
    
    static let fontSizeTable: [UIFontTextStyle: [UIContentSizeCategory: CGFloat]] = [
    .headline: [
        .accessibilityExtraExtraExtraLarge: 23,
        .accessibilityExtraExtraLarge: 23,
        .accessibilityExtraLarge: 23,
        .accessibilityLarge: 23,
        .accessibilityMedium: 23,
        .extraExtraExtraLarge: 23,
        .extraExtraLarge: 21,
        .extraLarge: 19,
        .large: 17,
        .medium: 16,
        .small: 15,
        .extraSmall: 14
    ],
    .subheadline: [
        .accessibilityExtraExtraExtraLarge: 21,
        .accessibilityExtraExtraLarge: 21,
        .accessibilityExtraLarge: 21,
        .accessibilityLarge: 21,
        .accessibilityMedium: 21,
        .extraExtraExtraLarge: 21,
        .extraExtraLarge: 19,
        .extraLarge: 17,
        .large: 15,
        .medium: 14,
        .small: 13,
        .extraSmall: 12
    ],
    .body: [
        .accessibilityExtraExtraExtraLarge: 53,
        .accessibilityExtraExtraLarge: 47,
        .accessibilityExtraLarge: 40,
        .accessibilityLarge: 33,
        .accessibilityMedium: 28,
        .extraExtraExtraLarge: 23,
        .extraExtraLarge: 21,
        .extraLarge: 19,
        .large: 17,
        .medium: 16,
        .small: 15,
        .extraSmall: 14
    ],
    .caption1: [
        .accessibilityExtraExtraExtraLarge: 18,
        .accessibilityExtraExtraLarge: 18,
        .accessibilityExtraLarge: 18,
        .accessibilityLarge: 18,
        .accessibilityMedium: 18,
        .extraExtraExtraLarge: 18,
        .extraExtraLarge: 16,
        .extraLarge: 14,
        .large: 12,
        .medium: 11,
        .small: 11,
        .extraSmall: 11
    ],
    .caption2: [
        .accessibilityExtraExtraExtraLarge: 17,
        .accessibilityExtraExtraLarge: 17,
        .accessibilityExtraLarge: 17,
        .accessibilityLarge: 17,
        .accessibilityMedium: 17,
        .extraExtraExtraLarge: 17,
        .extraExtraLarge: 15,
        .extraLarge: 13,
        .large: 11,
        .medium: 11,
        .small: 11,
        .extraSmall: 11
    ],
    .footnote: [
        .accessibilityExtraExtraExtraLarge: 19,
        .accessibilityExtraExtraLarge: 19,
        .accessibilityExtraLarge: 19,
        .accessibilityLarge: 19,
        .accessibilityMedium: 19,
        .extraExtraExtraLarge: 19,
        .extraExtraLarge: 17,
        .extraLarge: 15,
        .large: 13,
        .medium: 12,
        .small: 12,
        .extraSmall: 12
    ]
]
    
    final class func preferredDescriptor(textStyle: UIFont.TextStyle, styleBold: Bool = false) -> UIFontDescriptor {
        let contentSize = UIApplication.shared.preferredContentSizeCategory
                
        let fontFamily = styleBold ? FontFamily.preferredFontNameBold : FontFamily.preferredFontNameRegular
        
        guard let style = fontSizeTable[textStyle], let size = style[contentSize] else {
            return UIFontDescriptor(name: fontFamily, size: 16)
        }
        return UIFontDescriptor(name: fontFamily, size: size)
    }
}

myViewController.swift

myBoldLabel.font = UIFont(descriptor: .preferredDescriptor(textStyle: .body, styleBold: true), size: 0)

myNormalLabel.font = UIFont(descriptor: .preferredDescriptor(textStyle: .body), size: 0)

Swift 4: Custom scaled Font supporting Accessibility (German BITV)

//UIFont+CustomScaledFont.swift

import UIKit

extension UIFont {

    /// Scaled and styled version of any custom Font
    ///
    /// - Parameters:
    ///   - name: Name of the Font
    ///   - textStyle: The text style i.e Body, Title, ...
    /// - Returns: The scaled custom Font version with the given textStyle 
    static func scaledFont(name:String, textStyle: UIFont.TextStyle) -> UIFont {

        let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)

        guard let customFont = UIFont(name: name, size: fontDescriptor.pointSize) else {
            fatalError("Failed to load the \(name) font.")
        }

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

After successfully adding custom fonts to your project...

Print available font names:

for family in UIFont.familyNames {
    for name in UIFont.fontNames(forFamilyName: family) {
        print(name)
    }
 }

Setup Example:

myLabel.text = "My scaled custom Font"
myLabel.font = UIFont.scaledFont(name: "MyCustomFontName-Bold", textStyle: .title1)
myLabel.adjustsFontForContentSizeCategory = true   

Test with Accessibility Inspector (MacOS)


The problem with this method is that the text size won't change until the user goes back to the app, and then the user would see the old text size change to the new size, which is not ideal.

I share your thoughts that this would probably be a better UX, but I guess you are overthinking it a bit.

If you have a look at system provided Apps (e.g. Contacts) you will clearly see that the refresh is not done until the user goes back to the app, too.

By the way, you could refactor your code a bit for Swift 3:

extension UIFontDescriptor {

    private struct SubStruct {
        static var preferredFontName: String = "Avenir-medium"
    }

    static let fontSizeTable: [UIFontTextStyle: [UIContentSizeCategory: CGFloat]] = [
        .headline: [
            .accessibilityExtraExtraExtraLarge: 23,
            .accessibilityExtraExtraLarge: 23,
            .accessibilityExtraLarge: 23,
            .accessibilityLarge: 23,
            .accessibilityMedium: 23,
            .extraExtraExtraLarge: 23,
            .extraExtraLarge: 21,
            .extraLarge: 19,
            .large: 17,
            .medium: 16,
            .small: 15,
            .extraSmall: 14
        ],
        .subheadline: [
            .accessibilityExtraExtraExtraLarge: 21,
            .accessibilityExtraExtraLarge: 21,
            .accessibilityExtraLarge: 21,
            .accessibilityLarge: 21,
            .accessibilityMedium: 21,
            .extraExtraExtraLarge: 21,
            .extraExtraLarge: 19,
            .extraLarge: 17,
            .large: 15,
            .medium: 14,
            .small: 13,
            .extraSmall: 12
        ],
        .body: [
            .accessibilityExtraExtraExtraLarge: 53,
            .accessibilityExtraExtraLarge: 47,
            .accessibilityExtraLarge: 40,
            .accessibilityLarge: 33,
            .accessibilityMedium: 28,
            .extraExtraExtraLarge: 23,
            .extraExtraLarge: 21,
            .extraLarge: 19,
            .large: 17,
            .medium: 16,
            .small: 15,
            .extraSmall: 14
        ],
        .caption1: [
            .accessibilityExtraExtraExtraLarge: 18,
            .accessibilityExtraExtraLarge: 18,
            .accessibilityExtraLarge: 18,
            .accessibilityLarge: 18,
            .accessibilityMedium: 18,
            .extraExtraExtraLarge: 18,
            .extraExtraLarge: 16,
            .extraLarge: 14,
            .large: 12,
            .medium: 11,
            .small: 11,
            .extraSmall: 11
        ],
        .caption2: [
            .accessibilityExtraExtraExtraLarge: 17,
            .accessibilityExtraExtraLarge: 17,
            .accessibilityExtraLarge: 17,
            .accessibilityLarge: 17,
            .accessibilityMedium: 17,
            .extraExtraExtraLarge: 17,
            .extraExtraLarge: 15,
            .extraLarge: 13,
            .large: 11,
            .medium: 11,
            .small: 11,
            .extraSmall: 11
        ],
        .footnote: [
            .accessibilityExtraExtraExtraLarge: 19,
            .accessibilityExtraExtraLarge: 19,
            .accessibilityExtraLarge: 19,
            .accessibilityLarge: 19,
            .accessibilityMedium: 19,
            .extraExtraExtraLarge: 19,
            .extraExtraLarge: 17,
            .extraLarge: 15,
            .large: 13,
            .medium: 12,
            .small: 12,
            .extraSmall: 12
        ]
    ]

    final class func preferredDescriptor(textStyle: UIFontTextStyle) -> UIFontDescriptor {
        let contentSize = UIApplication.shared.preferredContentSizeCategory
        let style = fontSizeTable[textStyle]!
        return UIFontDescriptor(name: SubStruct.preferredFontName, size: style[contentSize]!)
    }
}

No need to cast to NSDictionary or NSNumber and get the floatValue indirectly.

This way your call site can use the following, more readable code:

func userChangedTextSize(notification: NSNotification) {
    label.font = UIFont(descriptor: .preferredDescriptor(textStyle: .body), size: 0)

}

Edit: As I am working on the same right now, I improved the above (on SO commonly seen solution) to something way easier.

import UIKIt

extension UIFont {

    private struct CustomFont {
        static var fontFamily = "Avenir"
    }

    /// Returns a bold version of `self`
    public var bolded: UIFont {
        return fontDescriptor.withSymbolicTraits(.traitBold)
            .map { UIFont(descriptor: $0, size: 0) } ?? self
    }

    /// Returns an italic version of `self`
    public var italicized: UIFont {
        return fontDescriptor.withSymbolicTraits(.traitItalic)
            .map { UIFont(descriptor: $0, size: 0) } ?? self
    }

    /// Returns a scaled version of `self`
    func scaled(scaleFactor: CGFloat) -> UIFont {
        let newDescriptor = fontDescriptor.withSize(fontDescriptor.pointSize * scaleFactor)
        return UIFont(descriptor: newDescriptor, size: 0)
    }

    class func preferredCustomFont(forTextStyle textStyle: UIFontTextStyle) -> UIFont {
        // we are using the UIFontDescriptor which is less expensive than creating an intermediate UIFont
        let systemFontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)

        let customFontDescriptor = UIFontDescriptor.init(fontAttributes: [
            UIFontDescriptorFamilyAttribute: CustomFont.fontFamily,
            UIFontDescriptorSizeAttribute: systemFontDescriptor.pointSize // use the font size of the default dynamic font
        ])

        // return font of new family with same size as the preferred system font
        return UIFont(descriptor: customFontDescriptor, size: 0)
    }

}

Usage

func userChangedTextSize(notification: NSNotification) {
    label.font = UIFont.preferredCustomFont(forTextStyle: .headline)
    // or in Bold / Italic:
    // label.font = UIFont.preferredCustomFont(forTextStyle: .headline).bolded
    // label.font = UIFont.preferredCustomFont(forTextStyle: .headline).italicized
}