Apple Engine

Apple, iPhone, iOS, その周辺のことについて

iOS 13 で Custom Fonts の API を使ってみる

今までの iOS では自前のフォント(カスタムフォント)の使用はアプリ内に制限されていたが、iOS 13 からは OS 機能として、自前のフォントのインストールと使用ができるようになり、
他のアプリでも自前のフォントが使用できるようになった。

フォントのインストール方法は、
アプリからフォントをインストールし他のアプリでも使用できるような形。
そのため、アプリをアンインストールするとインストールしたフォントが使えなくなる。

インストールしたフォントの確認は以下の場所でできる。
また、ユーザーによるフォントの削除もここでも行うことができる。

設定アプリ > 一般 > フォント
(Settings > General > Fonts)

f:id:x67x6fx74x6f:20191116040110p:plain

 

結局どう動いているのか?

詳しくは不明なので憶測だが、 CTFontManagerCopyRegisteredDescriptors という API があるということは、 設定アプリにフォントがインストールされるわけではなく、Today など Extensions のように XPC みたいなやつで、アプリのリソースを参照している可能性がある。

 

API できること

  • カスタムフォントのインストール
  • インストールされているフォントを選択するための UI の表示
  • インストールされているフォントの検索
  • カスタムフォントの削除

 

今回はカスタムフォントの削除の説明は行わない。
削除を行うケースがサブスクリプションなどの使用期間が決まっているものしか用途がないし、ユーザーで消すことができるので。
あと、アプリからの削除はフォントを指定して命令実行するだけなので。

 

カスタムフォント使用アプリで必ず行う設定

今回の様に他のアプリでもフォントを使用する場合は必ず行うことがあり、カスタムフォントのインストールやカスタムフォントの使用する場合、Entitlements ファイルが必要になる。

プロジェクトの Targets からアプリを選択し、「Signing & Capabilities」選択して、 左上プラスボタンの「Capabiliy」から「Fonts」を選択する

追加された「Fonts」で Privileges に 2 項目のチェックボックスがある。

  • Install Fonts
  • Use Installed Fonts

 

f:id:x67x6fx74x6f:20191116035940p:plain

 

「Install Fonts」
フォントをインストールするアプリで必須となり、このチェックがないと「does not have a necessary entitlement.」とほざいてエラーになる。

「Use Installed Fonts」
インストールされたフォントを使用できる様にする。
これにチェックがないとフォントを選択することができない。
こちらは資格情報がないなどのエラーが出ないので要注意。

 

API を使用する: フォントのインストール

f:id:x67x6fx74x6f:20191116040500p:plain

CFArray で設定した1つまたは複数のリソース、スコープの選択、他のアプリでの検知を有効にする Bool、フォントを登録する際に処理されるクロージャーを設定し、関数を実行するとフォントのインストールが開始される。

使用できる関数は以下の 3 つでバンドルの URL、フォントディスクリプター、アセット名から動作するものがあり、
CTFontManagerRegisterFontsWithAssetNames のみ CFBundleRef を選択する引数が増える。

  • CTFontManagerRegisterFontURLs
  • CTFontManagerRegisterFontDescriptors
  • CTFontManagerRegisterFontsWithAssetNames

 

今回は面倒なので CTFontManagerRegisterFontURLs を使用する。
CTFontManagerRegisterFontURLs の API の定義はこんな感じ

void CTFontManagerRegisterFontURLs(CFArrayRef fontURLs, CTFontManagerScope scope, bool enabled, bool (^registrationHandler)(CFArrayRef errors, bool done));

 

引数
引数 説明
fontURLs フォントURLの配列
scope 登録の可用性と有効期間を定義する定数
enabled CTFontManagerRequestFonts で検出可能にするかを示すブール値

 

scope (CTFontManagerScope)

フォント登録範囲の定義を表す。
また、ここでいうセッションとは macOS ではログインセッション、iOS では現在の起動セッションのことをいうらしい。

スコープ 説明
none フォントは登録されておらず、フォントディスクリプターでは一致しない。
そのためフォントの登録時に指定する有効なスコープではないとのこと。
process フォントは直接登録解除しない限り、現在のプロセスが継続中である場合に使用できる。
persistent フォントは現在のユーザーセッションのすべてのプロセスで使用でき、登録解除しない限り後続のセッションで使用できる。
session このフォントは現在のユーザーセッションで使用でき、以降のセッションでは使用できない。
このセッションスコープは macOS でのみ使用可能。

 

registrationHandler (CFArrayRef errors, bool done)

エラーが発見されたとき、または完了時に呼び出されるブロックのハンドラー。
errors パラメーターは CFError 参照の配列が含まれ、空の配列はエラーがないことを示す。

各エラーでは kCTFontManagerErrorFontURLsKey に対応するフォント URL の CFArray が含まれたエラーとなり、正常に登録されなかったフォントファイルを表す。

このハンドラーは登録プロセス中に複数回呼び出される場合があり、 登録プロセスが完了すると、done パラメーターは true を返す。

ブロックで操作を停止する場合、return で false を返す必要があり、 エラーを受け取った後に行うと良いだろうと思われる。

 

サンプルコード

メインバンドルから、スコープを persistent、このフォントを CTFontManagerRequestFonts で検出可能にする。

実行すると専用の UI が表示されインストールするか否かを操作できる。
試したところ、アプリで 1 回フォントをインストールをするとインストール用の UI が表示されなくなるため、複数インストールする場合は CFArray で複数設定した方が良さそう。
(しばらくするとキャッシュが切れ再度 UI が表示されるかも? サンプルコードでお試しあれ)

guard let mainBundleURL = Bundle.main.url(forResource: "フォント名", withExtension: "otf") else {
    print("File Not Found!")
    return
}

let fontURLArray = [mainBundleURL] as CFArray
CTFontManagerRegisterFontURLs(fontURLArray, .persistent, true) { (errors, done) -> Bool in
    
    if CFArrayGetCount(errors) > 0 {
        print(errors as Array)
        return false
    }
    
    if done {
        print("done")
        return true
    }

    return true
}

 

API を使用する: インストールされているフォントを UI で選択する

f:id:x67x6fx74x6f:20191116042332p:plain

iOS 13 で追加された UIFontPickerViewController を使用し、テーブルビューに列挙されたフォント名からフォントを選択する。

UIFontPickerViewController、その設定とデリゲートを作成し、普通の ViewController を様に present で表示する。

 

サンプルコード

class ViewController: UIViewController, UIFontPickerViewControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // フォントピッカーの表示設定
        let fontPickerConfig = UIFontPickerViewController.Configuration()
        
        // 複数のフォントフェイスを表示
        fontPickerConfig.includeFaces = true
        
        // 各フォント名にそのフォントを適応する (true だとシステムフォントで表示されるっぽい)
        fontPickerConfig.displayUsingSystemFont = false
        
        // 日本語フォントのみ表示
        fontPickerConfig.filteredLanguagesPredicate = UIFontPickerViewController.Configuration.filterPredicate(forFilteredLanguages: ["ja"])
        
        // フォントピッカーの設定
        let fontPicker = UIFontPickerViewController(configuration: fontPickerConfig)
        fontPicker.delegate = self

        // フォントピッカーの表示
        self.present(fontPicker, animated: true, completion: nil)
    }
    
    // UIFontPickerViewControllerDelegate - 選択後
    func fontPickerViewControllerDidPickFont(_ fontPicker: UIFontPickerViewController) {
        if let fontName = fontPicker.selectedFontDescriptor?.postscriptName {
            print("FontName: \(fontName)")
        }
    }

    // UIFontPickerViewControllerDelegate - キャンセル
    func fontPickerViewControllerDidCancel(_ viewController: UIFontPickerViewController) {
    }
}

 

UIFontPickerViewController.Configuration の説明

UIFontPickerViewController を表示する際、Configuration で値を設定すると特定の条件で表示をフィルタリングすることができる。

 

includeFaces

複数のフォントフェイスを表示するか否か。
false だと Regular っぽいもののみ表示し、true だとそのファミリー全て表示される。

 

displayUsingSystemFont

false にするとそのフォントでフォント名で表示される。
true にすると全てシステムフォントで表示される。

 

filteredTraits

以下、表示の際にフィルターできる特性。
例えば traitBold にすると Bold (太文字) のみ表示される。

  • traitItalic
  • traitBold
  • traitExpanded
  • traitCondensed
  • traitMonoSpace
  • traitVertical
  • traitUIOptimized
  • traitTightLeading
  • traitLooseLeading
  • classMask
  • classOldStyleSerifs
  • classTransitionalSerifs
  • classModernSerifs
  • classClarendonSerifs
  • classSlabSerifs
  • classFreeformSerifs
  • classSansSerif
  • classOrnamentals
  • classScripts
  • classSymbolic

 

filteredLanguagesPredicate

言語でフィルターする。
UIFontPickerViewController.Configuration のクラス関数 filterPredicate から言語を設定する。
複数の言語指定も可。

 

デリゲート

選択後とキャンセルしかない。
使用方法は上記のコードや最後のサンプルコードを参照。

 

API を使用する: インストールされているフォントの検索

f:id:x67x6fx74x6f:20191116042514p:plain
画像は API を使用し検知できなかった時の UI

CTFontManagerRequestFonts に検知したいフォントを UIFontDescriptor で設定し、CFArray に渡し、CTFontManagerRequestFonts で検索する。

CTFontManagerRequestFonts のブロック内で DispatchQueue を使用しているのは、CFArray が複数設定できるため複数回かつある程度の速さでコールされるため。

 

サンプルコード

let fontName = UIFontDescriptor(fontAttributes:
                                    [UIFontDescriptor.AttributeName.name: "98Font"])
        
let fontAskArray = [fontName] as CFArray

CTFontManagerRequestFonts(fontAskArray as CFArray) { (unresolved: CFArray) in
    DispatchQueue.main.async {
        print(unresolved)
    }
}

 

サンプルプロジェクト

一応、今回紹介したものを使用したプロジェクトファイルを作成してみた。

github.com

 

ちなみにインストールしたカスタムフォントを他のアプリで使用する場合は、使用中フォントが消されたり使えなくなる可能性もあるため NotificationCenter で kCTFontManagerRegisteredFontsChangedNotification などで調べる必要がある。
要注意。

 

まとめ

わりと手軽に macOS の様にフォントのインストールや選択や検索ができる様になった。
今後もこういう新規実装と手軽に使えるものをつくっていただきたいと思う。