I have been able to ship some image and asset catalogs in a Swift package with success using Xcode 12b1 and Swift 5.3. I am not having so much luck with using a custom .ttf file in a Swift Package.
I am loading a .ttf file in the manifest like so:
.target(
name: "BestPackage",
dependencies: [],
resources: [
.copy("Resources/Fonts/CustomFont.ttf"),
.process("Resources/Colors.xcassets")
]
),
And I noticed that there's no initializer on the Font
type in SwiftUI to include an asset from a module. For example, this works:
static var PrimaryButtonBackgroundColor: SwiftUI.Color {
Color("Components/Button/Background", bundle: .module)
}
However, there's no way to specify where a font is coming from. I was hoping that loading it into the module would emit it into the target for use, but no such luck:
static var PrimaryButtonFont: Font {
Font.custom("CustomFont", size: 34)
}
This does not load the font as expected. I'm investigating using a CoreText api to try and trick it into loading, but I feel like there should be an easier way. Any advice?
Update
Still no success but I was able to prove that the font is indeed inside the module
I wrote a method to get available font URLs from the module like so:
static func fontNames() -> [URL] {
let bundle = Bundle.module
let filenames = ["CustomFont"]
return filenames.map { bundle.url(forResource: $0, withExtension: "ttf")! }
}
Calling this method at runtime and printing the result yields this:
font names: [file:///Users/davidokun/Library/Developer/CoreSimulator/Devices/AFE4ADA0-83A7-46AE-9116-7870B883DBD3/data/Containers/Bundle/Application/800AE766-FB60-4AFD-B57A-0E9F3EACCDB2/BestPackageTesting.app/BestPackage_BestPackage.bundle/CustomFont.ttf]
I then tried to register the font for use in the runtime with the following method:
extension UIFont {
static func register(from url: URL) {
guard let fontDataProvider = CGDataProvider(url: url as CFURL) else {
print("could not get reference to font data provider")
return
}
guard let font = CGFont(fontDataProvider) else {
print("could not get font from coregraphics")
return
}
var error: Unmanaged<CFError>?
guard CTFontManagerRegisterGraphicsFont(font, &error) else {
print("error registering font: \(error.debugDescription)")
return
}
}
}
When I call it like so:
fontNames().forEach { UIFont.register(from: $0) }
I get this error:
error registering font: Optional(Swift.Unmanaged<__C.CFErrorRef>(_value: Error Domain=com.apple.CoreText.CTFontManagerErrorDomain Code=105 "Could not register the CGFont '<CGFont (0x600000627a00): CustomFont>'" UserInfo={NSDescription=Could not register the CGFont '<CGFont (0x600000627a00): CustomFont>', CTFailedCGFont=<CGFont (0x600000627a00): CustomFont>}))
Any more ideas are welcome.
I managed to import custom fonts using SPM, using this SO answer to help https://stackoverflow.com/a/36871032/5508175
Here is what I did. Create your package and add your fonts. Here is my Package.swift
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "MyFonts",
products: [
.library(
name: "MyFonts",
targets: ["MyFonts"]),
],
dependencies: [
],
targets: [
.target(
name: "MyFonts",
dependencies: [],
resources: [.process("Fonts")]),
.testTarget(
name: "MyFontsTests",
dependencies: ["MyFonts"]),
]
)
Here is my folder structure. I have all of my fonts contained in a folder called Fonts.
Inside MyFonts.swift
I do the following:
import Foundation // This is important remember to import Foundation
public let fontBundle = Bundle.module
This allows me access to the Bundle outside of the package.
Next I added the package to my project. It is a SwiftUI project with an AppDelegate.
didFinishLaunchingWithOptions
check to see if the font files are available (optional)So here is my AppDelegate:
import UIKit
import MyFonts
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// This prints out the files that are stored in the MyFont bundle
// Just doing this to check that the fonts are actually in the bundle
if let files = try? FileManager.default.contentsOfDirectory(atPath: fontBundle.bundlePath ){
for file in files {
print(file)
}
}
// This registers the fonts
_ = UIFont.registerFont(bundle: fontBundle, fontName: "FiraCode-Medium", fontExtension: "ttf")
_ = UIFont.registerFont(bundle: fontBundle, fontName: "FiraCode-Bold", fontExtension: "ttf")
_ = UIFont.registerFont(bundle: fontBundle, fontName: "FiraCode-Light", fontExtension: "ttf")
_ = UIFont.registerFont(bundle: fontBundle, fontName: "FiraCode-Regular", fontExtension: "ttf")
_ = UIFont.registerFont(bundle: fontBundle, fontName: "FiraCode-Retina", fontExtension: "ttf")
// This prints out all the fonts available you should notice that your custom font appears in this list
for family in UIFont.familyNames.sorted() {
let names = UIFont.fontNames(forFamilyName: family)
print("Family: \(family) Font names: \(names)")
}
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {}
}
// This extension is taken from this SO answer https://stackoverflow.com/a/36871032/5508175
extension UIFont {
static func registerFont(bundle: Bundle, fontName: String, fontExtension: String) -> Bool {
guard let fontURL = bundle.url(forResource: fontName, withExtension: fontExtension) else {
fatalError("Couldn't find font \(fontName)")
}
guard let fontDataProvider = CGDataProvider(url: fontURL as CFURL) else {
fatalError("Couldn't load data from the font \(fontName)")
}
guard let font = CGFont(fontDataProvider) else {
fatalError("Couldn't create font from data")
}
var error: Unmanaged<CFError>?
let success = CTFontManagerRegisterGraphicsFont(font, &error)
guard success else {
print("Error registering font: maybe it was already registered.")
return false
}
return true
}
}
Then in you ContentView
you can do something like this:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Text("Hello San Francisco")
Text("Hello FiraCode Medium").font(Font.custom("FiraCode-Medium", size: 16))
Text("Hello FiraCode Bold").font(Font.custom("FiraCode-Bold", size: 16))
Text("Hello FiraCode Light").font(Font.custom("FiraCode-Light", size: 16))
Text("Hello FiraCode Regular").font(Font.custom("FiraCode-Regular", size: 16))
Text("Hello FiraCode Retina").font(Font.custom("FiraCode-Retina", size: 16))
}
}
}
Which gives the following result:
I haven't tried this in a fully SwiftUI app, but you can follow the tutorial shown here on how to add an AppDelegate if you don't have one.
Obviously the printing of the files in the fontBundle
and the fonts that are installed are optional. They are just useful for debugging and for making sure that you have the correct font name The filename can differ quite considerably from the font name that you have to use to display the font. See my SO post about adding custom fonts:
I wondered if it was possible to create a function that was contained in the package and calling that would load the fonts. Apparently it is.
I updated to MyFonts.swift
to the following:
import Foundation
import UIKit
public func registerFonts() {
_ = UIFont.registerFont(bundle: .module, fontName: "FiraCode-Medium", fontExtension: "ttf")
_ = UIFont.registerFont(bundle: .module, fontName: "FiraCode-Bold", fontExtension: "ttf")
_ = UIFont.registerFont(bundle: .module, fontName: "FiraCode-Light", fontExtension: "ttf")
_ = UIFont.registerFont(bundle: .module, fontName: "FiraCode-Regular", fontExtension: "ttf")
_ = UIFont.registerFont(bundle: .module, fontName: "FiraCode-Retina", fontExtension: "ttf")
}
extension UIFont {
static func registerFont(bundle: Bundle, fontName: String, fontExtension: String) -> Bool {
guard let fontURL = bundle.url(forResource: fontName, withExtension: fontExtension) else {
fatalError("Couldn't find font \(fontName)")
}
guard let fontDataProvider = CGDataProvider(url: fontURL as CFURL) else {
fatalError("Couldn't load data from the font \(fontName)")
}
guard let font = CGFont(fontDataProvider) else {
fatalError("Couldn't create font from data")
}
var error: Unmanaged<CFError>?
let success = CTFontManagerRegisterGraphicsFont(font, &error)
guard success else {
print("Error registering font: maybe it was already registered.")
return false
}
return true
}
}
This meant that I could remove the extension from the AppDelegate and I don't have to register each font in the AppDelegate like I did before I just call registerFonts()
So my didFinishLaunchingWithOptions
now looks like this:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// This registers the fonts
registerFonts()
return true
}
Remember that you still have to import your package.
Here is a simplified version of @Andrew's answer. I have tested this in a 100% SwiftUI app on iOS and macOS; it does not require UIKit. Fonts registered in this way can be accessed from other dependent packages.
func registerFont(_ name: String, fileExtension: String) {
guard let fontURL = Bundle.module.url(forResource: name, withExtension: fileExtension) else {
print("No font named \(name).\(fileExtension) was found in the module bundle")
return
}
var error: Unmanaged<CFError>?
CTFontManagerRegisterFontsForURL(fontURL as CFURL, .process, &error)
print(error ?? "Successfully registered font: \(name)")
}
You should load your font and color assets into your package like this:
.target(
name: "BestPackage",
dependencies: [],
resources: [
.process("Resources")
]
),
From the docs:
If the given path represents a directory, Xcode applies the process rule recursively to each file in the directory.
If possible, use this rule instead of copy(_:).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With