iOS 安裝自定義字體

1.前期準(zhǔn)備

自定義字體須知

* 應(yīng)用必須包含 Fonts 相關(guān)的 entitlement。

在Capabilities中找到并添加 Fonts


截屏2021-04-21 下午2.01.53.png

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


截屏2021-04-21 下午2.03.13.png

*當(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
截屏2021-04-21 下午3.26.17.png

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ù)器地址:
lebron.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容