What can go wrong when using custom fonts in SwiftUI
Ensure that font weight gets correctly applied even if a custom font was not registered by the app developer
This blog post is targeting SDK developers who bundle custom fonts in their package/library/framework and these fonts shall be used in reusable SwiftUI views.
I will explain an issue observable when using font weight related view modifiers (like bold
) on a non-registered custom font.
Let's assume you added the Open Sans true type font files to your bundle
- OpenSans-Regular.ttf
- OpenSans-Bold.ttf
- ...
You also provide reusable SwiftUI views which shall automatically leverage the custom font. To do so you can pass the result of Font.custom(:size)
to the font
view modifier.
struct ReusableView: View {
var body: some View {
Text("Use Font.custom directly")
.font(.custom("OpenSans-Regular", size: 17))
.bold()
}
}
If you use the bold
view modifier then SwiftUI is smart enough to load OpenSans-Bold.
This all works well as long as the app developer did not forget to register the font in the app for use.
Font.registerOpenSans()
extension Font {
static func registerOpenSans() {
self.register(name: "OpenSans-Regular", withExtension: "ttf")
self.register(name: "OpenSans-Bold", withExtension: "ttf")
// ...
}
static func register(name: String, withExtension: String) {
let fontURL = Bundle.main.url(forResource: name, withExtension: withExtension)!
var error: Unmanaged<CFError>?
CTFontManagerRegisterFontsForURL(fontURL as CFURL, .process, &error)
if let error = error {
print(error.takeUnretainedValue())
}
}
}
So what if the app developer forgot to register the font?
You might expected that the bold font weight would still gets applied. But this is not the case. Apple Bug ?
To ensure that the font weight gets applied, no matter what, I recommend to use an extension. This extension either loads the font or fallbacks to the system font.
extension Font {
/// get "Open Sans" font or fallbacks to the system font in case the custom font was not registered
/// - Parameter size: size of font
/// - Returns: "Open Sans" font (or sytem font as fallback)
static func openSans(size: CGFloat) -> Font {
guard UIFont.familyNames.contains("Open Sans") else {
return Font.system(size: size)
}
return .custom("OpenSans-Regular", size: size)
}
}
This will ensure that SwiftUI will be able to apply font weight even to a fallback font.
struct ReusableView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Use extension with fallback handling")
.font(.openSans(size: 17))
.bold()
Text("Use Font.custom directly")
.font(.custom("OpenSans-Regular", size: 17))
.bold()
}
}
}
Of course the result is the same in case the font was registered.