Clean dynamic font API in Swift
There are generally a couple of ways to get custom fonts loaded into your apps.
- Include them in your bundle and load them at launch time via info.plist
- 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.
- Register the font with the system and load it into memory (only if its not already cached)
- If we’re targeting iOS 11+, scale the font’s size based on the current
UIContentSizeCategory
- 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:
- Typed font names
- Dynamic type support
- Dynamic font loading
- Font caching
Checkout the Gist to add dynamic font loading and caching to your apps.