Font Follow-up

April 4, 2017

In an earlier post I walked through the steps to add custom fonts to a project. My steps followed Apple’s recommended process of adding a list of the fonts to the project’s Info.plist. Michael Tsai posted links to another technique that’s interesting.

The technique is realized in FontBlaster by Arthur Ariel Sabintsev. FontBlaster is an open-source module for automatically loading any fonts found in an app’s bundle. It’s based on a technique that Marco Arment wrote about for loading encrypted font files.

The basic idea is to use Core Graphics to load and register the font files. Both Arment’s example and FontBlaster use CTFontManagerRegisterGraphicsFont to register a font based on the Data representing it. That makes sense for encrypted font files, but is more complicated then we need when we just have the fonts on disk.

I’m never a fan of taking on an external dependency, and I know where the fonts live in my app’s bundle, so rather than using the very clever FontBlaster, I wrote a little extension on UIFont to do the loading.

Assuming you’re using a Fonts directory like I suggested previously, and that you are copying the entire directory into your app bundle’s Resources, here’s a little extension on UIFont for loading the custom fonts.

extension UIFont {
    static func loadAppFonts() {
        guard let fontDirectory = Bundle.main.resourceURL?.appendingPathComponent("Fonts", isDirectory: true) else {
            assertionFailure("failed to find font directory")
            return
        }

        guard let fontFileURLs = try? FileManager.default.contentsOfDirectory(at: fontDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]) else {
            assertionFailure("failed to find font files")
            return
        }

        fontFileURLs.forEach { loadFont(from: $0) }
    }

    private static func loadFont(from url: URL) {
        var errorRef: Unmanaged<CFError>?
        let success = CTFontManagerRegisterFontsForURL(url as CFURL, CTFontManagerScope.process, &errorRef)
        if !success, let fontError = errorRef?.takeRetainedValue() {
            if let errorDescription = CFErrorCopyDescription(fontError) as String? {
                assertionFailure("Failed to load font from url “\(url)”: \(errorDescription)")
            } else {
                assertionFailure("Failed to load font from url “\(url)”")
            }
        }
    }
}

To use, just call UIFont.loadAppFonts() from your AppDelegate’s application(_:, didFinishLaunchingWithOptions:) method.

The loadAppFonts() method finds the Fonts directory in your Resources and gets the URL of every contained file. For each URL, it calls loadFont(from:). That method uses CTFontManagerRegisterFontsForURL to load and register the font in a single line of code. The other eight lines in the method are for error reporting.

With this in place, you can delete the UIAppFonts key and value from your Info.plist. Now whenever you want to change the fonts embedded in your app, it’s as simple as changing the files in the Fonts group of your Xcode project and recompiling.