Xcode 12b1和Swift Packages:自定义字体

15

我已经成功地使用Xcode 12b1和Swift 5.3在Swift包中运送了一些图像和资产目录。但是,在Swift包中使用自定义的.ttf文件时,我并没有那么幸运。

我正在manifest中这样加载.ttf文件:

.target(
  name: "BestPackage",
  dependencies: [],
  resources: [
    .copy("Resources/Fonts/CustomFont.ttf"),
    .process("Resources/Colors.xcassets")
  ]
),

我注意到 SwiftUI 的 Font 类型中没有初始化程序,可以从模块中包含资源。例如,下面的代码可以正常工作:

static var PrimaryButtonBackgroundColor: SwiftUI.Color {
  Color("Components/Button/Background", bundle: .module)
}

然而,无法指定字体的来源。我希望将其加载到模块中会将其发射到目标中供使用,但是没有这样的运气:

static var PrimaryButtonFont: Font {
  Font.custom("CustomFont", size: 34)
}

这不会按预期加载字体。我正在研究使用CoreText api来尝试欺骗它进行加载,但我觉得应该有更简单的方法。有任何建议吗?

更新

仍然没有成功,但我能够证明字体确实在模块内部。

我编写了一个方法来从模块中获取可用字体URL,如下所示:

  static func fontNames() -> [URL] {
    let bundle = Bundle.module
    let filenames = ["CustomFont"]
    return filenames.map { bundle.url(forResource: $0, withExtension: "ttf")! }
  }

在运行时调用此方法并打印结果会得到以下输出:

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]

然后我尝试使用以下方法将字体注册以在运行时中使用:

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
    }
  }
}

当我这样调用它时:

fontNames().forEach { UIFont.register(from: $0) }

我遇到了这个错误:

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>}))

欢迎提出更多想法。

2个回答

20

我使用SPM成功导入了自定义字体,参考了这个Stack Overflow的答案:https://dev59.com/amcs5IYBdhLWcg3wXSuU#36871032

以下是我的操作步骤。首先创建你的包并添加字体文件。这是我的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"]),
    ]
)

这是我的文件夹结构。我把所有字体都放在一个名为 Fonts 的文件夹中。

我的文件夹结构图像

在 MyFonts.swift 中,我执行以下操作:

import Foundation // This is important remember to import Foundation

public let fontBundle = Bundle.module

这使我可以在包外访问Bundle。
接下来,我将该包添加到我的项目中。这是一个带有AppDelegate的SwiftUI项目。
  • 导入MyFonts
  • didFinishLaunchingWithOptions中检查字体文件是否可用(可选)
  • 使用扩展将字体添加到UIFont中。
  • 打印字体以检查它们是否已安装(可选)
因此,这是我的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://dev59.com/amcs5IYBdhLWcg3wXSuU#36871032
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
    }
}

然后在你的ContentView中,你可以像这样做:

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))
        }
    }
}

这将产生以下结果:

iPhone SE上自定义字体的图像


注意事项

我没有在完全使用SwiftUI的应用程序中尝试过这个方法,但是如果您没有一个AppDelegate,您可以按照此处所示的教程添加一个。

显然,在fontBundle中打印文件和已安装的字体是可选的。它们只是用于调试和确保您拥有正确的字体名称,文件名可能与您用于显示字体的字体名称相差很大。请参阅我的SO帖子关于添加自定义字体:


更新

我想知道是否可能创建一个包含在包中的函数,并调用该函数来加载字体。显然是可以的。

我将MyFonts.swift更新为以下内容:

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
    }
}

这意味着我可以从AppDelegate中移除扩展名,并且不必像之前那样在AppDelegate中注册每个字体,只需调用registerFonts()即可。因此,我的didFinishLaunchingWithOptions现在看起来像这样:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    // This registers the fonts
    registerFonts()

    return true
}

请记住,您仍然需要导入您的包。


我尝试了你注册字体的方法,但它没有起作用。你能否提供一个示例项目的GitHub链接?先谢谢了! - Dheeraj Chahar
这里是一个 github 链接,包含自定义字体的 Swift 包。我已经检查过它在 UIKit 和 SwiftUI 中都可以正常工作。如果您仍然遇到困难,我建议您发布自己的 repo,以便我检查出问题所在。 - Andrew
1
我该如何在包内的视图控制器中使用包内的字体? - fullmoon

5
这是@Andrew回答的简化版本。我在iOS和macOS上的一个100% SwiftUI应用程序中进行了测试;它不需要UIKit。以这种方式注册的字体可以从其他相关包中访问。
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)")
}

你应该按照以下方式将字体和颜色资源加载到你的包中:

.target(
  name: "BestPackage",
  dependencies: [],
  resources: [
    .process("Resources")
  ]
),

来自文档:

如果给定的路径表示一个目录,则Xcode将递归地应用该进程规则到目录中的每个文件。

如果可能,请使用此规则而不是copy(_:)


1
这应该是被接受的答案,不再需要使用UIKit。除了上面的答案之外,你还需要在你的App类init方法中调用registerFont函数。如果我们可以像@main SPM可执行注释一样使用一些代码来执行SPM加载时的操作,那将会很棒,而不是从应用中调用registerFonts函数。 - Hadi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接