1.前期準(zhǔn)備
自定義字體須知
* 應(yīng)用必須包含 Fonts 相關(guān)的 entitlement。
在Capabilities中找到并添加 Fonts

Fonts 包含的兩個選項(xiàng)分別為
Install Fonts:安裝字體 (使app能夠在系統(tǒng)范圍內(nèi)提供字體)
Use Installed Fonts:使用已安裝的字體。(默認(rèn)情況下,應(yīng)用程序無法訪問用戶安裝的字體。應(yīng)用程序需要選擇這個功能,才能看到這些字體。)

:
*當(dāng)應(yīng)用被提交到商店時,需要同時提交應(yīng)用中提供給系統(tǒng)的所有字體。
(1)字體必須是應(yīng)用包的一部分,或者是按需加載的資源。
(2)支持的格式:ttf、otf、ttc(近代的,以及它的變體。不支持舊字體格式,如suitcase、postscript等)。
2.注冊字體
有三種注冊字體方法:
- CTFontManagerRegisterFontURLs (使用指向字體文件的FontURLs)
(RACSignal *)registerFontWithFontURL:(NSURL *)fontURL {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSURL *urls[] = {fontURL};
CFArrayRef fontURLs = CFArrayCreate(kCFAllocatorDefault, (void *)urls, (CFIndex)1, NULL);
CTFontManagerRegisterFontURLs(fontURLs, kCTFontManagerScopePersistent, true, ^bool(CFArrayRef _Nonnull errors, bool done) {
if (CFArrayGetCount(errors) > 0) {
// regist failed
CFErrorRef cfError = (CFErrorRef)CFArrayGetValueAtIndex(errors, 0);
NSError *error = (__bridge_transfer NSError *)cfError;
NSLog(@"Regist Font Failed: %@", [error localizedDescription]);
[subscriber sendNext:@(false)];
}
[subscriber sendNext:@(true)];
return nil;
});
return nil;
}];
}
- CTFontManagerRegisterFontDescriptors(通過使用字體描述符注冊字體)
- CTFontManagerRegisterFontsWithAssetNames(注冊存在于應(yīng)用程序中的字體資源)
- (void)registerFontWithFontAssetName:(NSString *)fontAssetName {
NSString *values[] = {fontAssetName};
CFArrayRef arrRef = CFArrayCreate(kCFAllocatorDefault, (void *)values, (CFIndex)1, NULL);
CTFontManagerRegisterFontsWithAssetNames(arrRef, nil, kCTFontManagerScopePersistent, true, ^bool(CFArrayRef _Nonnull errors, bool done) {
if (CFArrayGetCount(errors) > 0) {
// regist failed
CFErrorRef cfError = (CFErrorRef)CFArrayGetValueAtIndex(errors, 0);
NSError *error = (__bridge_transfer NSError *)cfError;
NSLog(@"Regist Font Failed: %@", [error localizedDescription]);
return false;
}
return true;
});
}
移除注冊字體有兩張字體方法
- CTFontManagerUnregisterFontURLs
- (RACSignal *)unregisterFontWithFontURL:(NSURL *)fontURL {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSURL *urls[] = {fontURL};
CFArrayRef fontURLs = CFArrayCreate(kCFAllocatorDefault, (void *)urls, (CFIndex)1, NULL);
CTFontManagerUnregisterFontURLs(fontURLs, kCTFontManagerScopePersistent, ^bool(CFArrayRef _Nonnull errors, bool done) {
if (CFArrayGetCount(errors) > 0) {
// regist failed
CFErrorRef cfError = (CFErrorRef)CFArrayGetValueAtIndex(errors, 0);
NSError *error = (__bridge_transfer NSError *)cfError;
NSLog(@"Regist Font Failed: %@", [error localizedDescription]);
[subscriber sendNext:@(false)];
return false;
}
[subscriber sendNext:@(true)];
return true;
});
return nil;
}];
}
- CTFontManagerUnegisterFontDescriptors
+ (RACSignal *)unregisterFontWithFontDescriptor:(UIFontDescriptor *)fontDescriptor {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
CTFontDescriptorRef fontDescriptors[] = {(__bridge CTFontDescriptorRef)fontDescriptor};
CTFontManagerUnregisterFontDescriptors(CFArrayCreate(kCFAllocatorDefault, (void *)fontDescriptors, (CFIndex)1, NULL), kCTFontManagerScopePersistent, ^bool(CFArrayRef _Nonnull errors, bool done) {
if (CFArrayGetCount(errors) > 0) {
// regist failed
CFErrorRef cfError = (CFErrorRef)CFArrayGetValueAtIndex(errors, 0);
NSError *error = (__bridge_transfer NSError *)cfError;
NSLog(@"Unregist Font Failed: %@", [error localizedDescription]);
[subscriber sendNext:@(false)];
}
[subscriber sendNext:@(true)];
return nil;
});
return nil;
}];
}
查看已經(jīng)注冊的字體有兩種方法
- CTFontManagerCopyRegisteredDescriptors (字體提供app用于訪問已注冊的字體)
+ (RACSignal *)getRegisteredFonts {
NSMutableArray *hasRegisterFonts = [NSMutableArray array];
CFArrayRef registerdDescriptors = CTFontManagerCopyRegisteredFontDescriptors(kCTFontManagerScopePersistent, true);
for (CFIndex i = 0; i < CFArrayGetCount(registerdDescriptors); i ++) {
CTFontDescriptorRef fontDescriptorRef = CFArrayGetValueAtIndex(registerdDescriptors, i);
UIFontDescriptor *fontDescriptor = (__bridge_transfer UIFontDescriptor *)fontDescriptorRef;
// save registered fonts ...
[hasRegisterFonts addObject:fontDescriptor];
}
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:hasRegisterFonts];
return nil;
}];
}
- CTFontManagerRequestFonts
CFArrayRef registerdDescriptors = CTFontManagerCopyRegisteredFontDescriptors(kCTFontManagerScopePersistent, true);
CTFontManagerRequestFonts(registerdDescriptors, ^(CFArrayRef _Nonnull unresolvedFontDescriptors) {
})
3.注意事項(xiàng)
* 字體提供應(yīng)用,僅可管理(移除)自己注冊的字體,無法管理其他字體提供應(yīng)用所注冊的字體。
* 字體無法被重復(fù)注冊。
* 當(dāng)字體提供應(yīng)用被卸載,其所注冊字體將一并被移除。
* 字體使用者,實(shí)例化字體時,需要檢查所使用字體是否存在,因?yàn)樗赡芤驗(yàn)楦鞣N原因而被改變。
* 注冊字體在何處被管理:字體提供應(yīng)用程序,或者【通用-設(shè)置-字體】。
4.實(shí)際開發(fā)
在對字體按照有了系統(tǒng)和api方法學(xué)習(xí)和了解的情況下,在進(jìn)行實(shí)際開發(fā)的時候,會遇到讓人意想不到的一些問題,下面就闡述一下,我在開發(fā)自定義字體時,遇到的痛點(diǎn).
如上所訴
* 字體必須是應(yīng)用包的一部分,或者是按需加載的資源。
* 支持的格式:ttf、otf、ttc(近代的,以及它的變體。不支持舊字體格式,如suitcase、postscript等)。
* 系統(tǒng)不允許字體提供app任意安裝字體,字體需提交到應(yīng)用商店,并經(jīng)過一個類似 macOS 中的 Font Book 的簡單驗(yàn)證流程
那么在實(shí)際開發(fā)中,如果自定義的字體包數(shù)量比較多,而且字體包比較大的情況下,會導(dǎo)致打出來的ipa包非常的大,在進(jìn)行了調(diào)研的后,目前找到一個可行性的方法,但還有待于實(shí)際項(xiàng)目的驗(yàn)證,就是采用On-Demand Resources(ODR)方式,按需加載資源.
為什么用ODR
- Smaller app size. app體積更小,在上傳至 App Store 的時候 ipa 的體積會更小。
- Lazy loading of app resources. 資源延遲加載,app 有一些只在特定情景下使用的資源,當(dāng)應(yīng)用可能要進(jìn)入這些場景時,會請求這些資源。例如,在一個有很多關(guān)卡的游戲中,用戶只需要當(dāng)前關(guān)卡和下一關(guān)卡的資源。
- Remote storage of rarely used resources. 不常用資源的遠(yuǎn)程存儲,app 有一些很少使用的資源,當(dāng)需要這些資源時會去請求它們。例如,當(dāng) app 第一次打開時會展示一個教程,而這個教程之后就可能不會在用到。app 在第一次啟動時請求教程的資源,這之后只在需要展示教程或者添加了新功能才去請求該資源。
- Remote storage of in-app purchase resources. 應(yīng)用內(nèi)購買資源的遠(yuǎn)程存儲,app 提供包含額外資源的應(yīng)用內(nèi)購買。app 會在啟動完成后請求已購買模塊的資源。例如,用戶在一個鍵盤 app 內(nèi)購買了 SuperGeeky 表情包。應(yīng)用程序會在啟動完成后請求表情包的資源。
如何使用ODR
在target 選擇 "Build Settings" 搜索 "enable on" 打開為 YES
On-Demand Resource 的三種標(biāo)簽
-
Initial install tags此種標(biāo)簽的資源,會隨著 app 從 App Store 下載而下載,但是會影響 app 的 ipa 大小,也就是說此種資源會包含在 ipa 內(nèi)。 -
Prefetch tag order.此種標(biāo)簽會在 app 下載后,開始下載相應(yīng)的資源,下載是存在順序的,后面會說明。此種資源并不會影響 ipa 的大小,也就是說此種資源并不包含在 ipa 內(nèi)。 -
Dowloaded only on demand. 此種標(biāo)簽下的資源,會在必要的時候,主動觸發(fā)下載,這是我們開發(fā)者自己控制下載時機(jī)的
目前來看顯然是 Dowloaded only on demand 更適合解決我們在現(xiàn)實(shí)開發(fā)中遇到的問題.
如何使用 On-Demand Resource
系統(tǒng)提供了相應(yīng)的獲取按需加載資源的類,NSBundleResourceRequest ,其提供了2個重要的方法:
-
beginAccessingResourcesWithCompletionHandler:會從 App Store 下載這些資源; -
conditionallyBeginAccessingResourcesWithCompletionHandler:不會下載資源;
eg:
// Create an NSSet object with the desired tags
NSSet *tags = [NSSet setWithObjects: @"New", @"New-1"];
// Use the shorter initialization method as all resources are in the main bundle
resourceRequest = [[NSBundleResourceRequest alloc] initWithTags:tags];
// Request access to the tags for this resource request
[resourceRequest beginAccessingResourcesWithCompletionHandler:
^(NSError * __nullable error)
{
// Check if there is an error
if (error) {
// There is a problem so update the app state
self.resourcesLoaded = NO;
// Should also inform the user of the error
return;
}
// The associated resources are loaded
self.resourcesAvailable = YES;
}
];
- 上述 tags 中的 New 、New-1 為上述在 Prefetched Tag Order 中創(chuàng)建的標(biāo)簽名稱。如何沒有錯誤,New 、New-1 中對應(yīng)的資源就可以使用了,使用資源文件的方式和正常加載 Bundle 中的文件沒有任何差異。
- 注:不要使用同一個 NSBundleResourceRequest 實(shí)例多次請求訪問資源,否則讓會 Crash。
判斷資源是否已經(jīng)下載可以使用conditionallyBeginAccessingResourcesWithCompletionHandler:來判斷,如果回調(diào)為 NO ,則可以調(diào)用 beginAccessingResourcesWithCompletionHandler: 來下載資源。
調(diào)試 On-Demand Resource
在開發(fā)階段,我們將如何調(diào)試,這在研究時真的很費(fèi)勁,官方文檔并沒有確切的文字說明,WWDC 視頻有提到,但是不確切。搜集大量針對文檔和視頻的解讀,以及自己不斷試錯,總結(jié)如下:
-
Initial install tags. :Testflight 測試; -
Prefetch tag order. :Testflight 測試、直接 Debug 測試; -
Dowloaded only on demand. 可以使用私有服務(wù)區(qū)存儲資源測試,但上架時需要使用 APP Store 服務(wù);
使用 Dowloaded only on demand. 時,測試需要使用私有服務(wù)器,需要設(shè)置服務(wù)器地址:
