Ship Custom Fonts within a Swift Package
I found out that you can bundle TrueType Font files (.ttf
) with Swift Packages just fine. It’s not as declarative as adding Info.plist
entries to your app, but the code is very simple.
First, add fonts as managed resources under e.g. Sources/PACKAGENAME/Resources/Fonts/
(last subdirectory is optional):
// ...
targets: [
.target(
name: "MyLibrary",
resources: [
.copy("Resources/Fonts/MyFont.ttf"),
],
),
],
// ...
Then use Bundle.module
to access these resources. I went with a switch to support both command-line builds and Xcode-managed app targets, to get to the resource URL’s.
With these, you can tell the low-level Core Text API to make the font files available for use. The Core Text Font Manager is responsible for knowing which font resource is available in your app, and you can tell it to add locally bundled fonts to the stack:
func registerFontsFromBundle(named names: [String]) {
let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle.main
#endif
}()
let fontURLs = names
.compactMap { bundle.url(forResource: $0, withExtension: "ttf") }
CTFontManagerRegisterFontURLs(fontURLs as CFArray, .process, true) { errors, done in
// CTFontManager.h points out that the CFArray, if not empty, contains CFError values.
let errors = errors as! [CFError]
guard errors.isEmpty else {
preconditionFailure("Registering font failed: \(errors.map(\.localizedDescription))")
}
return true // true: should continue; false: should stop
}
}
Usage
Call this early in the app lifecycle, e.g. NSApplicationDelegate.applicationDidFinishLaunching(_:)
.
Afterwards, you can use the fonts like you would use system fonts or fonts bundled with app targets:
let myFont = NSFont(named: "MyFont")
In SwiftUI, your App
type’s .init
works (or the application delegate if you have any):
// Consider an extension or other means to load and group your fonts.
let myFont = SwiftUI.Font(CTFont("MyFont" as CFString, size: 20))
@main
struct FontTestApp: App {
init() {
// Call as early as possible:
registerFontsFromBundle(named: ["MyFont"])
}
var body: some Scene {
WindowGroup {
VStack {
Text("Hello, world!")
.font(myFont)
}.padding()
}
}
}
Works on macOS and iOS. And you see – you have to drop to Core Text for CTFont
anyway there.
Even SwiftUI Previews can access this, by the way!
Error Handling
Note that I crash the app with a preconditionFailure
on error.
Trying to register the same font twice would be an error you can easily reproduce. You may not want to crash the app there, but when I do ship fonts, I expect them to work and to use them, so crashing if the resource is not available is a sensible marker of a programmer error.
You might want to forward errors instead, but that would require a callback/closure because you can’t throw
from within the CTFontManagerRegisterFontURLs
callback.
Other Details
CTFontManagerRegisterFontURLs
is the more modern Core Text API to register multiple fonts programmatically at once.
The .proccess
scope tells the font manager to make the fonts available for your app’s current process, and not e.g. for the user’s current session (where fonts would be available until they log out). In most cases this is likely what you would expect to happen.
The code is based on this Gist and the similar answer on the Apple Dev forums, but using font URL arrays instead of manually getting from URL
to CGDataProvider
to CGFont
to CTFontManagerRegisterGraphicsFont
for each of the fonts. I went with the linked approaches to try everything and then explored other API in the Core Text framework (aka I used Xcode auto-completion on “CTFontManagerRegister
”).
The older CTFontManagerUnregisterFontsForURLs
works just as well but returns an error array pointer instead of passing errors by value to a closure. Use the modern API to avoid the manual memory management.