Xcode:在动态框架中使用自定义字体

47

我将自定义字体添加到了一个框架中。我按照所有步骤进行了操作,但它没有起作用。

我可以在界面生成器(Interface Builder)中设置字体,但在构建项目时,模拟器/设备上并没有展示这个字体。


1
你试过我回答中的方法吗?我已经在几个使用自定义字体捆绑动态框架的应用程序中使用它,而无需将字体添加到主项目中。 - user4151918
1
对于寻找最佳答案的人,请注意CTFontManagerRegisterGraphicsFont不应该是使用的类,它甚至在自己的文档中告诉你不要使用它。请使用底部基于CTFontManagerRegisterFontsForURL的答案。 - itMaxence
10个回答

47

我来晚了一点,但是我采用了PetahChristian的解决方案,并以扩展的形式创建了一个Swift版本,它对我很有效。 我发现当您尝试使用常规方法使用字体名称和大小获取字体时,它总是在主束中查找字体文件,并且没有可以接受包标识符作为参数的方法。 如果苹果能够提供这个功能就好了。

Swift:

public extension UIFont {

    public static func jbs_registerFont(withFilenameString filenameString: String, bundle: Bundle) {

        guard let pathForResourceString = bundle.path(forResource: filenameString, ofType: nil) else {
            print("UIFont+:  Failed to register font - path for resource not found.")
            return
        }

        guard let fontData = NSData(contentsOfFile: pathForResourceString) else {
            print("UIFont+:  Failed to register font - font data could not be loaded.")
            return
        }

        guard let dataProvider = CGDataProvider(data: fontData) else {
            print("UIFont+:  Failed to register font - data provider could not be loaded.")
            return
        }

        guard let font = CGFont(dataProvider) else {
            print("UIFont+:  Failed to register font - font could not be loaded.")
            return
        }

        var errorRef: Unmanaged<CFError>? = nil
        if (CTFontManagerRegisterGraphicsFont(font, &errorRef) == false) {
            print("UIFont+:  Failed to register font - register graphics font failed - this font may have already been registered in the main bundle.")
        }
    }

}

使用示例:

UIFont.jbs_registerFont(
    withFilenameString: "Boogaloo-Regular.ttf",
    bundle: Bundle(identifier: "com.JBS.JBSFramework")!
)

4
应该使用CTFontManagerRegisterFontsForURL代替CTFontManagerRegisterGraphicsFont,让系统处理文件内容的加载到内存中。 - Koen.
确实,这个答案已经过时了。...RegisterFontsForURL...RegisterFontURLs是可用的,应该被使用(正如在...GraphicsFont的文档中指定的那样)。请给底部提到它的答案点赞。 - itMaxence

17
以下解决方案允许您自动加载特定扩展名的所有字体:
static func registerFonts() {
    let fonts = Bundle(for: 
OrientationMonitor.self).urls(forResourcesWithExtension: "ttf", subdirectory: nil)
    fonts?.forEach({ url in
        CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
    })
}

确保将OrientationMonitor替换为您框架中存在的类。


我采用了你明确注册所有内容的方法,因为(在Xcode 12中)似乎没有在Main.storyboard中的任何字体都没有被注册(即使我做其他一切正确...)。我的小简化:let fonts = Bundle.main.urls(...) - tiritea
1
你甚至可能想使用 CTFontManagerRegisterFontURLs(...) 方法来替代 forEach - itMaxence
这个完美地工作了。非常感谢你。 - sanket pahuja

15

这是我对John答案的版本,展示了如何在有大量字体的情况下调用该函数

import Foundation

extension UIFont {

    @nonobjc static var loadAllFontsDO: dispatch_once_t = 0

    class func initialsAvatarFont() -> UIFont {
        loadAllFonts()
        if let retval = UIFont(name: "MyFontName", size: kInitialsAvatarFontSize) {
            return retval;
        } else {
            return UIFont.systemFontOfSize(kInitialsAvatarFontSize)
        }
    }

    class func loadAllFonts() {
        dispatch_once(&loadAllFontsDO) { () -> Void in
            registerFontWithFilenameString("thefontfilename.ttf", bundleIdentifierString: "nameOfResourceBundleAlongsideTheFrameworkBundle")
            // Add more font files here as required
        }
    }

    static func registerFontWithFilenameString(filenameString: String, bundleIdentifierString: String) {
        let frameworkBundle = NSBundle(forClass: AnyClassInYourFramework.self)
        let resourceBundleURL = frameworkBundle.URLForResource(bundleIdentifierString, withExtension: "bundle")
        if let bundle = NSBundle(URL: resourceBundleURL!) {
            let pathForResourceString = bundle.pathForResource(filenameString, ofType: nil)
            let fontData = NSData(contentsOfFile: pathForResourceString!)
            let dataProvider = CGDataProviderCreateWithCFData(fontData)
            let fontRef = CGFontCreateWithDataProvider(dataProvider)
            var errorRef: Unmanaged<CFError>? = nil

            if (CTFontManagerRegisterGraphicsFont(fontRef!, &errorRef) == false) {
                NSLog("Failed to register font - register graphics font failed - this font may have already been registered in the main bundle.")
            }
        }
        else {
            NSLog("Failed to register font - bundle identifier invalid.")
        }
    }
}

感谢您提供这个好的解决方案。有一个问题,当传递bundleIdentifierString时,“nameOfResourceBundleAlongsideTheFrameworkBundle”是什么意思?能否提供一个例子。非常感谢!谢谢。 - Daisy R.
@DaisyR。我写这篇文章已经有很长时间了,但是:这个解决方案适用于当您正在构建框架和资源包时。换句话说,您正在给其他开发人员提供两个文件,foo.frameworkfoo.bundle。nameOfResourceBundle...指的是foo.bundle - xaphod
谢谢,不幸的是,在我的情况下这并没有起作用,在Swift 3中dispatch_once不存在,但可能通过扩展可以实现。我不明白为什么需要这个块来加载字体。我知道这是构建框架时的解决方案,这就是我找到这篇文章的原因 :) - Daisy R.

12

Swift 4:

这可能是一个旧的线程,但已经更新了@xaphod,因为所有静态和全局变量都是使用dispatch_once进行延迟初始化。

extension UIFont {

// load framework font in application
public static let loadAllFonts: () = {
    registerFontWith(filenameString: "SanFranciscoText-Regular.otf", bundleIdentifierString: "Fonts")
    registerFontWith(filenameString: "SanFranciscoText-Medium.otf", bundleIdentifierString: "Fonts")
    registerFontWith(filenameString: "SanFranciscoText-Semibold.otf", bundleIdentifierString: "Fonts")
    registerFontWith(filenameString: "SanFranciscoText-Bold.otf", bundleIdentifierString: "Fonts")
    registerFontWith(filenameString: "SanFranciscoText-LightItalic.otf", bundleIdentifierString: "Fonts")
}()

//MARK: - Make custom font bundle register to framework
static func registerFontWith(filenameString: String, bundleIdentifierString: String) {
    let frameworkBundle = Bundle(for: MSAlertController.self)
    let resourceBundleURL = frameworkBundle.url(forResource: bundleIdentifierString, withExtension: "bundle")
    if let url = resourceBundleURL, let bundle = Bundle(url: url) {
        let pathForResourceString = bundle.path(forResource: filenameString, ofType: nil)
        if let fontData = NSData(contentsOfFile: pathForResourceString!), let dataProvider = CGDataProvider.init(data: fontData) {
            let fontRef = CGFont.init(dataProvider)
            var errorRef: Unmanaged<CFError>? = nil
            if (CTFontManagerRegisterGraphicsFont(fontRef!, &errorRef) == false) {
                print("Failed to register font - register graphics font failed - this font may have already been registered in the main bundle.")
            }
        }
    }
    else {
        print("Failed to register font - bundle identifier invalid.")
    }
}
}

然后你可以在 appDelegate 中调用 UIFont.loadAllfont


10

我在Swift 4.2中混合了不同的答案,因此要感谢想出这个方法的人们!

import UIKit
import Foundation

extension UIFont {

    private class MyDummyClass {}

    static func loadFontWith(name: String) {
        let frameworkBundle = Bundle(for: MyDummyClass.self)
        let pathForResourceString = frameworkBundle.path(forResource: name, ofType: "ttf")
        let fontData = NSData(contentsOfFile: pathForResourceString!)
        let dataProvider = CGDataProvider(data: fontData!)
        let fontRef = CGFont(dataProvider!)
        var errorRef: Unmanaged<CFError>? = nil

        if (CTFontManagerRegisterGraphicsFont(fontRef!, &errorRef) == false) {
            NSLog("Failed to register font - register graphics font failed - this font may have already been registered in the main bundle.")
        }
    }

    public static let loadMyFonts: () = {
        loadFontWith(name: "Exo-Black")
        loadFontWith(name: "Exo-Bold")
        loadFontWith(name: "Exo-Regular")    
    }()
}

然后在Appdelegate中调用

UIFont.loadMyFonts

CTFontManagerRegisterGraphicsFont(_:_:)的文档中可以看出,由文件支持的字体应该使用CTFontManagerRegisterFontsForURL(_:_:_:)进行注册。由于这个示例是从文件加载字体,我认为这不是使用此API的预期方式。 - B Roy Dawson

8

您可以通过在动态框架中实现+load方法来加载和使用捆绑的自定义字体。

load方法中,您可以定位存储在 bundle 中的字体,然后注册它们。这样就可以在应用程序中使用它们,而不必在主项目中指定它们。

+ (void)load
{
    static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
        // Dynamically load bundled custom fonts

        [self bible_loadFontWithName:kBIBLECustomFontBoldName];
        [self bible_loadFontWithName:kBIBLECustomFontBoldItalicName];
        [self bible_loadFontWithName:kBIBLECustomFontItalicName];
        [self bible_loadFontWithName:kBIBLECustomFontRegularName];
    });
}

+ (void)bible_loadFontWithName:(NSString *)fontName
{
     NSString *fontPath = [[NSBundle bundleForClass:[BIBLE class]] pathForResource:fontName ofType:@"otf"];
     NSData *fontData = [NSData dataWithContentsOfFile:fontPath];

     CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)fontData);

     if (provider)
     {
        CGFontRef font = CGFontCreateWithDataProvider(provider);

         if (font)
         {
             CFErrorRef error = NULL;
             if (CTFontManagerRegisterGraphicsFont(font, &error) == NO)
             {
                 CFStringRef errorDescription = CFErrorCopyDescription(error);
                 NSLog(@"Failed to load font: %@", errorDescription);
                 CFRelease(errorDescription);
             }

             CFRelease(font);
        }

        CFRelease(provider);
    }
}

2
你需要使用#import <CoreText/CoreText.h>才能使用CTFontManagerRegisterGraphicsFont - MrTristan
谢谢您,亲切的先生。 - Anish Kumar

8

我在 UIFont 上创建了一个扩展,将注册在束中找到的所有字体(指定类型的字体)。请注意,由于这会注册字体,因此无需在框架的 Info.plist 中包括“应用程序提供的字体”。

extension UIFont {

    private static var fontsRegistered: Bool = false

    static func registerFontsIfNeeded() {
        guard
            !fontsRegistered,
            let fontURLs = someBundle?.urls(forResourcesWithExtension: "otf", subdirectory: nil)
        else { return }

        fontURLs.forEach({ CTFontManagerRegisterFontsForURL($0 as CFURL, .process, nil) })
        fontsRegistered = true
    }
}

确保在尝试创建自定义字体实例之前调用 registerFontsIfNeeded()。例如:

...
registerFontsIfNeeded()

let myCustomFont = UIFont(name: "My-Custom-Font-Name", size: 20)
...

也许不需要使用forEach,可以使用...RegisterFontURLs函数,是吧?无论如何,回答很好。 - itMaxence

7

我找到了一种非常简单易懂的注册字体的方法,这里没有提到:

func registerFont(with fontName: String) {
    guard let url = Bundle(for: BundleToken.self).url(forResource: fontName, withExtension: nil),
        CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil) else {
            fatalError("Failed to register font: \(font.fileName)")
    }
}

private final class BundleToken {}

谢谢!我发现这个API可以与UIFontPickerViewController一起使用,而使用CTFontManagerRegisterGraphicsFont注册的字体由于某些原因不会出现在列表中。 - Jordan H

2

鉴于现在可以直接将资源包含在框架包中,我能够使用Swift 4实现此操作:

Typography.swift(在我的框架中)

import Foundation

private class MyDummyClass {}

func loadFontWith(name: String) {
  let frameworkBundle = Bundle(for: MyDummyClass.self)
  let pathForResourceString = frameworkBundle.path(forResource: name, ofType: "otf")
  let fontData = NSData(contentsOfFile: pathForResourceString!)
  let dataProvider = CGDataProvider(data: fontData!)
  let fontRef = CGFont(dataProvider!)
  var errorRef: Unmanaged<CFError>? = nil

  if (CTFontManagerRegisterGraphicsFont(fontRef!, &errorRef) == false) {
    NSLog("Failed to register font - register graphics font failed - this font may have already been registered in the main bundle.")
  }
}

public func loadMyFonts() {
  loadFontWith(name: "ComicSansPro-Regular")
  loadFontWith(name: "ComicSansPro-Medium")
  loadFontWith(name: "ComicSansPro-Bold")
  loadFontWith(name: "ComicSansPro-ExtraBold")
}

最终,我需要在使用该框架的应用程序的AppDelegate.swift中的didFinishLaunchingWithOptions方法中调用loadMyFonts方法。


就是这样。PS:不要忘记将字体添加到复制包资源中,以便通过资源路径找到它们。 - Okhan Okbay

1
我想分享我的答案。我的项目如下所示:
  • 主iOS应用程序(Swift)

    • 动态框架(Obj-C)

      • Fonts.bundle(包含所有字体的包)

      • UIFont类别

      • NSBundle类别

      • 其他框架类

    • 应用程序类(ViewControllers,Models,CoreData等)

我的目标是使主应用程序调用动态框架上的单个方法来加载字体,而无需更改Info.plist或向主目标添加字体文件/包。
@import CoreText;

@implementation NSBundle (Fonts)

+ (NSBundle *)fontsBundle {
    // The only way I could find to do this is to hard code the sub-path. Using pathForResource doesn't seem to find Fonts.bundle, nor its contents\
    // This way the host app doesn't need to copy Fonts.bundle
    NSString *path = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:@"/Frameworks/<YourFrameworkName>.framework/Fonts.bundle"];
    NSBundle *bundle = [NSBundle bundleWithPath:path];
    if (bundle == nil) {
        NSLog(@"Warning: Fonts.bundle could not be loaded. Have you included it in your target?");
    }
    return bundle;
}

- (BOOL)loadFonts {

    NSArray<NSString *> *names = @[
        @"GothamRnd-Bold",
        @"GothamRnd-BoldItal",
        @"GothamRnd-Book",
        @"GothamRnd-BookItal",
        @"GothamRnd-Light",
        @"GothamRnd-LightItal",
        @"GothamRnd-MedItal",
        @"GothamRnd-Medium",
    ];

    __block NSInteger failCounter = 0;
    [names enumerateObjectsUsingBlock:^(id _Nonnull name, NSUInteger idx, BOOL *_Nonnull stop) {
        NSString *fontPath = [self pathForResource:name ofType:@"otf"];
        NSData *inData = [NSData dataWithContentsOfFile:fontPath];
        CFErrorRef error;
        CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)inData);
        CGFontRef font = CGFontCreateWithDataProvider(provider);

        if (!CTFontManagerRegisterGraphicsFont(font, &error)) {
            if (error) {
                NSLog(@"Failed to load font at path: %@", fontPath);
                failCounter++;
            }
            CFStringRef errorDescription = CFErrorCopyDescription(error);
            NSLog(@"Failed to load font: %@", errorDescription);
            CFRelease(errorDescription);
        }
        CFRelease(font);
        CFRelease(provider);

    }];

    return failCounter == 0;
}

@end

这段代码唯一的不足之处在于您必须硬编码字体包(Fonts.bundle)的路径。我无法找到任何组合的NSBundle方法来自动定位Fonts.bundle文件。例如,这些方法都不能返回路径:

NSString *pathToBundle = [[NSBundle mainBundle] pathForResource:@"Fonts" ofType:@"bundle"];
NSString *pathToFont = [[NSBundle mainBundle] pathForResource:@"MyFont" ofType:@"ttf"];

除了硬编码(永远不会改变)之外,这个对我来说已经足够好了。现在我可以轻松地为所有客户端应用进行界面设计。


我不理解你的“fontsBundle”方法。是什么调用了它? - picciano
预期这样调用:if([[NSBundle fontsBundle] loadFonts] == NO) { /error/} - VaporwareWolf

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