Clean dynamic font API in Swift

For more on this subject, check out my related post:
https://152percent.com/blog/2018/7/3/type-...

There are generally a couple of ways to get custom fonts loaded into your apps.

  1. Include them in your bundle and load them at launch time via info.plist
  2. Dynamically load fonts after launch, even from a remote url

Loading many fonts via your info.plist is generally impractical as it has a detrimental effect on your apps launch time. However it removes the need for any code to load the fonts into memory and guarantees they exist well before you need to access them. As an added bonus, fonts loaded this way can be defined in XIB's and Storyboard's.

Dynamically loading fonts has the advantage of being able to load the fonts on-demand and you can even download them from a remote url. This approach also doesn't require you to touch your info.plist.

I recently worked on an app that contained around 15-20 fonts, most of which were not required during the early stages of the apps lifecycle. So I opted for dynamically loading the fonts on-demand.

For the purposes of this article, I'd like to focus on a clean API pattern that I've used across various apps.

API Implementation

When dynamically loading fonts, we generally need 3 pieces of information. The font's name, filename and extension.

public protocol FontCacheDescriptor: Codable {
    var fontName: String { get }
    var fileName: String { get }
    var fileExtension: String { get }
}

Now we have a type that describes our custom font. In most cases on iOS, we deal with TrueType fonts, so let's define a default implementation for that.

public extension FontCacheDescriptor {
    public var fileExtension: String {
        return "ttf"
    }
}

The approach I'm going to suggest makes use of enum types. With that knowledge in hand, lets add a another extension to make use of the rawValue when our enum is RawRepresentable.

  public extension FontCacheDescriptor where Self: RawRepresentable, Self.RawValue == String {
      public var fontName: String {
          return rawValue
      }

      public var fileName: String {
          return rawValue
      }
  }


Here's comes the meat of this API. In order to load our custom font we need to perform the following tasks.

  1. Register the font with the system and load it into memory (only if its not already cached)
  2. If we're targeting iOS 11+, scale the font's size based on the current UIContentSizeCategoy
  3. Make a descriptor for the font

So lets add a convenience initializer to UIFont.

extension UIFont {

    public convenience init(descriptor: FontCacheDescriptor, size: CGFloat) {
        FontCache.cacheIfNeeded(named: descriptor.fileName, fileExtension: descriptor.fileExtension)
        let size = UIFontMetrics.default.scaledValue(for: size)
        let descriptor = UIFontDescriptor(name: descriptor.fontName, size: size)
        self.init(descriptor: descriptor, size: 0)
    }

}

API Usage

With all the code in place, we can now easily create clean APIs around our custom fonts for use in our UI code.

Lets say we have a font called Graphik and it has 4 variants.

extension UIFont {
    
    // The `rawValue` MUST match the filename (without extension)
    public enum Graphik: String, FontCacheDescriptor {
        case regular = "GraphikAltWeb-Regular"
        case medium = "GraphikAltWeb-Medium"
        case regularItalic = "GraphikAltWeb-RegularItalic"
        case mediumItalic = "GraphikAltWeb-MediumItalic"
    }
    
    /// Makes a new font with the specified variant, size
    public convenience init(graphik: Graphik, size: CGFloat) {
        self.init(descriptor: graphik, size: size)
    }
    
}

Now our UI code can simply create an instance of this font.

let font = UIFont(graphik: .regular, size: 16)

Conclusion

This API provides a clean and simple approach that provides various benefits:

  1. Typed font names
  2. Dynamic type support
  3. Dynamic font loading
  4. Font caching
The code for loading and caching the fonts can be found on GitHub, via the button at the bottom of this post. 

If you liked this post or want to discuss more, leave a comment below or find me on Twitter