Quantumleap
6506 words
33 minutes
iOSアプリでのフォントの扱い方
2023-06-11

フォント#

フォントを利用するには、

  1. MobileConfig を利用したプロファイル方式
  2. Assets.xcassets に埋め込むアセット方式
  3. Documents から読み込むドキュメント方式
  4. iOS13 以降対応したフォント方式

の四つがあります。

呼び方は適当なのでそこは気にしないでください。

各方式の比較#

それぞれの大雑把な比較は以下のとおりです。

プロファイルアセットドキュメントフォント
権利難/易
利用---iOS13 以降
拡張子ttf, otfttf, woff, woff2ttf, woff, woff2ttf, woff, woff2

ただし、フォント方式に関しては今回は別のフォントアプリでフォントをインストールするようなことを考えていないので、URL からダウンロードしてきたフォントファイルをフォントとしてインストールして利用することを考えています。

よって、ドキュメント方式とフォント方式は今回の場合は二つで一つということになります。

権利#

いちばん大事なのがここで、再配布を禁止していたり商用利用が不可だったりするフォントは多数あります。

プロファイルにしろ、アセットにしろ.mobileconfigassets.xcassetsに組み込まなければいけないので、これらの方式ではすべてのフォントを自由に配布したりすることはできません。

iOS アプリにフォントを組み込む記事を検索すると日本語でも英語でもほとんどがassets.xcassetsを利用した方法を紹介していますが、この方法は使えないわけです。

とはいえ、それぞれどのような違いがあるのかを調べてみることにしました。

プロファイル方式はttfないしはotfしかサポートしていませんが、バンドル方式はwoffwoff2が利用できました。これらは圧縮率が高くてオススメです

テスト用アプリ#

どのフォントが利用できるかはUIFont.familyNamesを参照すればわかります。

ここに表示されないフォントはどうやってもアプリ内から利用できないので、これでフォントが利用可能になっているかどうかを判断できます。

ContentView#

struct ContentView: View {
    var body: some View {
        NavigationView(content: {
            List(content: {
                NavigationLink(destination: {
                    FontListPicker()
                }, label: {
                    Text("Font Lists")
                })
            })
        })
    }
}

UIFontPickerViewControllerSwiftUIでそのまま利用できないので、UIViewControllerRepresentableを利用します。

struct FontListPicker: UIViewControllerRepresentable {
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIFontPickerViewController {
        let controller: UIFontPickerViewController = UIFontPickerViewController()
        controller.delegate = context.coordinator
        return controller
    }

    func updateUIViewController(_ uiViewController: UIFontPickerViewController, context: Context) {
    }

    class Coordinator: NSObject, UIFontPickerViewControllerDelegate {
        private let parent: FontListPicker

        init(_ parent: FontListPicker) {
            self.parent = parent
        }

        func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
            guard let descriptor = viewController.selectedFontDescriptor,
                  let name: String = descriptor.fontAttributes[.family] as? String,
                  let font: UIFont = UIFont(name: name, size: UIFont.systemFontSize)
            else {
                return
            }
            UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: true)
        }
    }
}

これでFontListPickerにインストールしたフォントが表示されるようになれば利用可能、ということです。

Capabilities#

フォント方式でインストールしたフォントを利用するにはCapabilitiesからFontsを有効化してUse Installed FontsInstall Fontsにチェックを入れます。

Use Installed Fonts にチェックを入れるとバイナリにフォントが同梱されていないと Invalid Binary で AppStore Connect から弾かれるので注意

プロファイル方式#

Apple Configurator を使ってフォントをインストールする方式です。

iOS13 以前ではこれが利用されていたようなので、どうなるのか検証してみます。

導入の方法を書いていると長いので、先駆者の方の記事を載せておきます。

iOS にフォントをいっぱいインストールしたいを読めばオリジナルフォントをインストールするのは難しくないと思います。

検証#

で、この方式でインストールしたフォントは何故かシミュレータだと反映されませんでした。

何故なのかはわかりません。

しかし、実機であれば全くの設定不要でフォントが読み込めます。Info.plist を編集する必要もないですし、何も必要ないです。

.font(.custom(FAMILY_NAME, size: UIFont.systemFontSize))みたいなことを書けばそれで終わりです。

シミュレータiPhone/iPad
Use Installed Fonts不要不要
フォントの読み込み不可
設定画面表示なしなし
Xcode 設定不要不要

つまり、上のような結果になります。

構成プロファイルを作成するのが手間なこと以外はものすごく簡単だと思います。

必要なもの#

さて、今回のケースだと構成プロファイルを作成するのに何が必要なのでしょうか。

  1. Apple Configurator 2
  2. FontForge
  3. スプラトゥーン用のフォント

このうちなんと Apple Configurator 2 は Windows 版が提供されていない。つまり、この時点で Windows ユーザーは構成プロファイルを利用したフォントのインストールができないことになります。

とはいえ、構成プロファイルは単純にフォントの Base64 バイナリが XML に貼り付けられているだけなので適当に TS でコードを書けば構成プロファイルを作成するウェブサイトを作成するのは難しくないと思います。それを転送するのがまた手間ですけれど…

マージするフォントは以下の URL から手に入ります。Web で普通に公開されているのでとても楽です。2 用と 3 用がありますが、韓国語と中国語を利用しないのであれば 2 用のフォントで十分です。

https://app.splatoon2.nintendo.net/fonts/bundled/ab3ec448c2439eaed33fcf7f31b70b33.woff2
https://app.splatoon2.nintendo.net/fonts/bundled/0e12b13c359d4803021dc4e17cecc311.woff2
https://app.splatoon2.nintendo.net/fonts/bundled/da3c7139972a0e4e47dd8de4cacea984.woff2
https://app.splatoon2.nintendo.net/fonts/bundled/eb82d017016045bf998cade4dac1ec22.woff2

四つのファイルはそれぞれスプラ 1 用の日本語と英語、スプラ 2 用の日本語と英語のフォントになっています。

日本語フォントには英語のフォントが全く含まれていないので、これらをマージしてしまえば良いことになります。

で、マージするには FontForge を利用するのが一番楽です。 手順は長いので割愛します。

マージしたフォントを ttf として出力し、構成プロファイルに組み込んでインストールすればフォントを利用することができます。

アセット方式#

何も考えないなら一番楽なのがこれです。

ただし、アプリ自体にフォントをバンドルしなければいけない性質上、権利関係をクリアするのはほぼ不可能です。

配布不可なフォントを利用したい場合にはこの方式は使えません。

シミュレータiPhone/iPad
Use Installed Fonts不要不要
フォントの読み込み
設定画面表示なしなし
Xcode 設定必要必要

アセット方式でのカスタムフォント利用方法についてはいろいろ記事があるのですが、パッと目についたカスタムフォントを使用する方法をご紹介しておきます。

多分 typo だと思うのでタイトルを修正しておきました

Google Fonts などを利用するのであればこの方法で良いかもしれません。

ドキュメント方式#

ドキュメント方式はアプリが持つDocuments以下にフォントのファイルをダウンロードして、そのフォントを読み込むタイプの対応方法です。

ユーザーが勝手にフォントをダウンロードしてくるのでアプリ自体にフォントをバンドルする必要はありません。

シミュレータiPhone/iPad
Use Installed Fonts不要不要
フォントの読み込み
設定画面表示ありあり
Xcode 設定ありあり

アプリ自身がインストールしたフォントを利用する場合はUse Installed Fontsは不要だが、Install Fontsは必要でプロセスにインストールするだけであればInstall Fontsは不要

Install Fonts YesInstall Fonts No
Use Installed Fonts Yes全て利用可他のアプリでインストールしたフォントが利用可
Use Installed Fonts Noアプリがインストールしたフォントは利用可.process のみ利用可

なにやらややこしいのですが、とりあえずどちらもチェックを入れて損はないです。ただし、Install Fonts を Yes にするのであればバイナリに必ずフォントを同梱してください。

この方式ができればめんどくさい構成プロファイルの作成が省略できて楽なのですが、この方式を導入するに当たって難しい点を挙げると、

  1. Core Text Functionsに関するドキュメントが少ない
  2. 起動時にフォントを読み込んで登録する必要がある
    • 他の二つの方式ではアプリ自体及び構成プロファイルがフォントを自動で登録してくれていましたが、本方式ではアプリ起動時に登録する必要があります
    • 多分ですがAppDelegateで登録しておけばよいです
  3. 同一の FamilyName を持つフォントに対する読み込み方法がわからない
  4. インストールダイアログがでない
    • 本当に謎で、端末リセット直後の一回だけでたけどそれ以後音沙汰がないです
    • 複数同時にインストールしようとするとそうなるのかもしれない
  5. FamilyName が異なるフォントがある
    • 後述します

と、ハードルがものすごく高いのですがこの方式ができれば一番楽です。なぜならフォントの URL もそのフォントが持つ FamilyName の情報も全てわかっているからです。

FamilyName 問題#

かなり大きな問題で、スプラトゥーン 1 用の漢字フォントはROWDayStdという FamilyName が設定されているにも関わらず、ひらがなと英語のフォントはSplatoon1とうい FamilyName になっているからです。

つまり、単純にフォントが持っている FamilyName で登録してしまうと漢字とひらがなが混在しているテキストに.font()を当てると漢字かひらがなのどちらか一方は普通のフォントで表示されてしまうという問題が発生します。これを解決するにはフォントを読み込んだときに FamilyName を変更して登録する必要があるのですが、それをやるとCTFontManagerError.duplicatedNameで怒られます。

SwiftUI でのフォントの読み込み方法を変えるかCTFontManagerあたりを上手いことやる必要があると思うのですがいかんせんドキュメントが少なすぎて手探り感が半端ないです。

ダイアログ問題#

何故か端末をリセットした最初の一回だけでます。

.processでインストールした場合にはでてこないので、.persistentを指定する必要があると思います。

まだ調査不足です。

フォント関連のメソッド#

インストール#

  • CTFontManagerRegisterFontsForURL(CFURL, CTFontManagerScope, UnsafeMutablePointer<Unmanaged<CFError>?>?) -> Bool
    • 指定された URL のフォントを指定されたパラメータで登録する
    • CTFontManagerScope=.process以外は登録に失敗する
  • CTFontManagerRegisterFontDescriptors(CFArray, CTFontManagerScope, Bool, ((CFArray, Bool) -> Bool)?)
    • 指定されたフォント一覧を指定されたパラメータで登録する
    • CTFontManagerScope=.persistent以外は登録に失敗する
  • CTFontManagerRegisterFontsWithAssetNames(CFArray, CFBungle, CTFontManagerScope, Bool)
    • 指定されたファミリーネーム一覧を指定されたパラメータで登録する
    • CTFontManagerScope=.persistent以外は登録に失敗する

アンインストール#

  • CTFontManagerUnregisterFontDescriptors(CFArray, CTFontManagerScope, ((CFArray, Bool) -> Bool)?)
    • 指定されたファミリーネーム一覧を指定されたパラメータで解除する

取得#

  • CTFontManagerCopyRegisteredFontDescriptors(CTFontManagerScope, Bool) -> CFArray
    • 指定されたパラメータで登録されているすべてのフォントを取得して返す
    • フォント一覧取得
  • CTFontDescriptorCopyAttribute(CTFontDescriptor, CFString) -> CFTypeRef?
    • 指定されたフォントの指定されたパラメータを返す
    • フォントのパラメータ取得

その他#

  • CTFontManagerSetAutoActivationSetting(CFString?, CTFontManagerAutoActivationSetting)
    • 指定されたバンドル ID のフォントを自動でアクティベーションする

パッと見ただけだと何がなんだかわからないと思うので解説します。

まず、フォントのインストール・アンインストールに関して二つのパラメータがあります。

それがscope: CTFontManagerScopeenabled: Boolですが、これらは何も考えずにそれぞれ.persistenttrueを指定するようにしましょう。特にenabled=falseを指定するとえらくめんどくさいです。

CTFontManagerScope#

フォントの影響力を表す。

  • none
    • スコープなし
  • process
    • 現在のプロセスで unregistered が呼ばれるまで有効
    • アプリ終了などでプロセスがキルされるとアンインストールされる
    • 設定のフォントからインストールしたフォントが見れない
    • プロセスキルでアンインストールされる以外は構成プロファイルと似ている
  • persistent
    • ユーザーのすべてのプロセスで unregistered が呼ばれるまで有効
    • アプリを終了してもインストール状態が継続
    • 設定のフォントからインストールしたフォントが見れる
  • session
    • macOS のみ有効なので今回は考慮しない
  • user
    • persistent と同じ

Enabled#

登録されているフォントのうち有効なものを返すか無効なものを返すかを表す。

enabled=falseでフォントをインストールすると、設定画面からフォントが表示されず、かといって再度インストールしようとすると1 files have already been registered in the specified scope.エラーが返ってくる。

falseを設定する意味が今のところ見えないので、通常はtrueで良い。

CTFontDescriptor#

登録されているフォントの情報は以下の四つ

  • CTFontRegistrationUserInfoAttribute
  • NSCTFontFileURLAttribute
  • NSFontFamilyAttribute
  • NSFontNameAttribute

これを FontForge で確認できるフォント情報を見比べてみる。

Splatoon 1Splatoon 2
FontnameRowdyStd-EB-KanjiKurokaneStd-EB-Kanji
Family NameFowdyStdKurokaneStd
Name For HumansRowdyStd-EB-KanjiKurokaneStd-EB-Kanji
NSFontFamilyAttributeFOT-Rowdy Std EBFOT-Kurokane Std EB
NSFontNameAttributeRowdyStd-EBKurokaneStd-EB
CTFontRegistrationUserInfoAttributeab3ec448c2439eaed33fcf7f31b70b33da3c7139972a0e4e47dd8de4cacea984
NSCTFontFileURLAttributeURLURL

すると何故か全然一致しないという謎の状態が発生した。

CTFontRegistrationUserInfoAttributeはファイル名なので、これだけを信用したほうが良い気がする。

もしくは、予め Fontname か Family Name がわかっているものでないと利用するのは難しいと思われる。

/// CTFontRegistrationUserInfoAttribute
CTFontDescriptorCopyAttribute(descriptor, kCTFontRegistrationUserInfoAttribute) as? String
/// NSCTFontFileURLAttribute
CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute) as? String
/// NSFontNameAttribute
CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute) as? String
/// NSFontFamilyAttribute
CTFontDescriptorCopyAttribute(descriptor, kCTFontAttributeName) as? String

多分上のようなコードでCTFontDescriptorからデータが取ってこれるが、最悪辞書なのでゴリ押しでもとれます。

メソッド詳細#

CTFontDescriptorCopyAttribute#

CTFontDescriptorから安全にプロパティを取得するメソッド。使い方は先程解説した通り。

func CTFontDescriptorCopyAttribute(
    _ descriptor: CTFontDescriptor,
    _ attribute: CFString
) -> CFTypeRef?

CTFontManagerRegisterFontDescriptors#

[CTFontDescriptor]を一括でインストールするメソッド。とはいえCTFontDescriptorになっている時点で普通はインストールが完了しているはずなので、ここはまだ使い方がしっかり理解できていないと思われる。

今回のサンプルプログラムでは利用しなかった。

func CTFontManagerRegisterFontDescriptors(
    _ fontDescriptors: CFArray,
    _ scope: CTFontManagerScope,
    _ enabled: Bool,
    _ registrationHandler: ((CFArray, Bool) -> Bool)?
)

CTFontManagerRegisterFontURLs#

func CTFontManagerRegisterFontURLs(
    _ fontURLs: CFArray,
    _ scope: CTFontManagerScope,
    _ enabled: Bool,
    _ registrationHandler: ((CFArray, Bool) -> Bool)?
)

フォントの URL を指定して一括でインストールするメソッド。便利なのに非推奨。

.persistent.processのどちらでも使えると思われるが、.persistentにしたいなら以下のCTFontManagerRegisterFontsWithAssetNamesを利用するのが無難。

CTFontManagerRegisterFontsWithAssetNames#

Assets.xcassetsに登録されているフォントをインストールする。

func CTFontManagerRegisterFontsWithAssetNames(
    _ fontAssetNames: CFArray,
    _ bundle: CFBundle?,
    _ scope: CTFontManagerScope,
    _ enabled: Bool,
    _ registrationHandler: ((CFArray, Bool) -> Bool)?
)

ちょっとわかりにくいので少し解説。

  • fontAssetNames
    • インストールしたいフォントのファイル名(拡張子不要)
  • bundle
    • 何も考えずにCFBundleGetMainBundle()を指定すれば良い。

CFBundle は NSBundle とは互換性がないようなのでBundle.mainなどは利用できない

Xcode はビルド時にアセットの階層構造が全てなくなるのでバンドルされているファイルを取得して指定されたファイル名のフォントを取ってきているようだ。

端末リセット直後の初回インストール時のみダイアログが出現する。

CTFontManagerUnregisterFontDescriptors#

フォントマネージャを使ってフォントをアンインストールするメソッド。

func CTFontManagerUnregisterFontDescriptors(
    _ fontDescriptors: CFArray,
    _ scope: CTFontManagerScope,
    _ registrationHandler: ((CFArray, Bool) -> Bool)?
)

ここでアンインストールすべきフォントを正しくとってこないと、全てのフォントが消えます。

guard let fontDescriptors: [CTFontDescriptor] = CTFontManagerCopyRegisteredFontDescriptors(.persistent, true) as? [CTFontDescriptor]
else {
    return
}

のようなコードで登録されているフォントを全て取得してからフィルターをかけてアンインストールすべきフォントを正しく取得しましょう。

なお、インストールされていないフォントをアンインストールしようとしても特にエラーはでません。

CTFontManagerUnregisterFontURLs#

func CTFontManagerUnregisterFontURLs(
    _ fontURLs: CFArray,
    _ scope: CTFontManagerScope,
    _ registrationHandler: ((CFArray, Bool) -> Bool)?
)

指定された URL のフォントを一括でアンインストールするメソッド。

インストールされてないフォントをアンインストールしようとするとエラーが返る。

CTFontManagerRegisterFontsForURL#

指定された URL のフォントをインストールするメソッド。

func CTFontManagerRegisterFontsForURL(
    _ fontURL: CFURL,
    _ scope: CTFontManagerScope,
    _ error: UnsafeMutablePointer<Unmanaged<CFError>?>?
) -> Bool

URL を指定できるということはもちろんDocumentsからフォントをインストールすることもできますが.process以外が効きません。

実行した場合のエラーコードも載せておきます。

  • .none
    • Someone attempted to (un)register one or more fonts with CTFontManager using scope kCTFontManagerScopeNone. That's not a valid scope for (un)registration, so we'll use kCTFontManagerProcess instead. This message will not be logged again.
  • .persistent
    • kCTFontManagerScopePersistent is not supported by this function. Use API with registrationHandler block parameter.

ちなみにプロセス実行中しか効かないので、アプリを終了すればフォントは自動的にunregisteredされます。

よって、起動時に毎回インストールを実行する必要があります。やるならAppDelegateで実行するのが良いかと思われる。

CTFontManagerSetAutoActivationSettingを使えば自動でインストールするようにできるかもしれないけれど、まだ未調査です。

CTFontManagerCopyRegisteredFontDescriptors#

指定されたスコープでインストールされているフォントを取ってきます。

func CTFontManagerCopyRegisteredFontDescriptors(
    _ scope: CTFontManagerScope,
    _ enabled: Bool
) -> CFArray

返り値はCFArrayとなっていますが、実質的に[CTFontDescriptor]と同じです。

補足説明#

先人の記事に拠ればResource Tagにも追加すると書いてあるが、これは結局ファイルの存在チェックにしか使えず、バンドルしているならフォントがあるのは当たり前の話であるし、バンドルしていないならそもそも Resource Tag の値は設定できないので事実上やってもやらなくても良い設定になっています。

現状、書かなくてもフォントはインストールできるので特にこの手順は不要かと思います。

既存の問題を解消するために#

さて、一番の理想としてはフォントはどこかのサーバーからダウンロードしてきてそれを永続インストールしたいわけです。

で、永続インストールするためには.processしか使えないCTFontManagerRegisterFontsForURLではなく.persistentが利用できるCTFontManagerRegisterFontsWithAssetNamesの方が便利です。

  1. CTFontManagerRegisterFontsWithAssetNamesDocumentsからインストールする
  2. CTFontManagerRegisterFontURLsDocumentsからインストールする
  3. CTFontManagerRegisterFontsForURLを起動時に実行する

ということで候補に上がるのはこの三つ。

最初は 2 で終わりじゃないかと思っていたのですが、実行してみると以下のような306 エラーが出ました。

Error Domain=com.apple.CoreText.CTFontManagerErrorDomain Code=306
The file is not in an allowed location. It must be either in the application's bundle or an on-demand resource.

つまり、指定された URL が良くなくて、バンドルかオンデマンドリソースにあるフォントを指定しろとあります。まあ確かに外部のへんてこなフォントをインストールできては困るので、これは仕方ないかもしれません。

で、バンドルに含めるのは再三ダメだといってきたので残るはオンデマンドリソースになります。

なんだこれとなったのですが、調べてみるとソシャゲとかでよくある「アプリインストール時には要らないけれど起動時にダウンロードされる追加コンテンツ」であることがわかりました。

じゃあこれで解決かと思ったのですが、オンデマンドリソースは Apple のサーバーからか自分のサーバーからしかインストールすることができません。Apple のサーバーにファイルを置いておくのはバンドルしているのと変わりませんし、自分のサーバーであってもそれは同じことです。

結局のところ「アプリが無条件に信頼しているところからしか.persistentとしてフォントはインストールできないよ」ということになります。

したがって 1, 2 の方式は無理だということがわかり、必然的に 3 の方式ということになります。

CTFontManagerSetAutoActivationSettingで自動登録はできないのか#

無理です。

macOS 10.6+以降しか対応してませんでした。よって iOS では不可能です。

登録されたフォント情報を取得する#

CTFontManagerCopyAvailablePostScriptNames()CTFontManagerCopyAvailableFontFamilyNames()でインストールされているフォントがとってこれるので取ってきます。

CTFontManagerCopyRegisteredFontDescriptorsでとってこれるんじゃないのと思ったのですが、取ってこれませんでした。

どうも設定のフォントのところに登録されているフォントしかとってこれないっぽい

.processでインストールした場合はあそこに表示されないので仕方ないかなという気もします。

/// PostScriptNames
[KurokaneStd-EB, RowdyStd-EB, Splatoon1, Splatoon2]
/// FamilyNames
[FOT-Kurokane Std EB, FOT-Rowdy Std EB, Splatoon1, Splatoon2]

で、取得した結果が上のような感じでした。この値はこれから使うことになるので覚えておきます。

スコープと利用可能なメソッド#

どれが使えてどれが使えないかがわかりにくいのでまとめました。

メソッド.persistent.process
CTFontManagerRegisterFontsForURL-Documents
CTFontManagerRegisterFontURLs-Documents
CTFontManagerRegisterFontsWithAssetNamesAssets.xcassets-
CTFontManagerUnregisterFontsForURL-Documents
CTFontManagerUnregisterFontURLs-Documents
CTFontManagerUnregisterFontDescriptorsAssets.xcassets-
CTFontManagerCopyRegisteredFontDescriptorsOKNG

オンデマンドリソースを使わないのであれば.persistentはバンドルされた署名済みのフォントにしか使えません。Documentsなどのファイルを指定するとCTFontManagerErrorDomain Code=306が発生します。

また、その逆でバンドルされたフォントを.processで登録することもできません。登録しようとするとInvalid argumentが返ります。

バンドルされたフォント#

バンドルしているならフォントの情報は全てわかっているはずなので何も考えずにインストールではCTFontManagerRegisterFontsWithAssetNamesを使っておいて、アンインストールするときにはCTFontManagerUnregisterFontDescriptorsCTFontManagerCopyRegisteredFontDescriptorsを組み合わせて利用すると良いでしょう。

CTFontManagerCopyRegisteredFontDescriptors.persistentでインストールされたフォントしか取ってこれないので.processでインストールしたフォントはこの方法ではアンインストールできません

取得したフォント#

外部から取得したフォントは署名がないのでシステムにインストールすることはできません。

メソッド.persistent.process
CTFontManagerRegisterFontsForURL-Documents
CTFontManagerRegisterFontURLs-Documents
CTFontManagerUnregisterFontsForURL-Documents
CTFontManagerUnregisterFontURLs-Documents

よって、インストールとアンインストールは上の四つのメソッドを使うことになります。何も考えずにFileManager.defaultとかで URL を取得して[CFURL]を経由してCFArrayに渡すだけ、難しいところもないです。

CTFontManagerRegisterFontURLsCTFontManagerRegisterFontsForURLの完全上位互換です。

フォントのマージ#

そしていちばん大事なところがここ、フォントのマージができるのかどうか。

ここまでいろいろ書いてきましたが、結局それらは理解を深めるために書いてきただけで、ここのマージができないと何の解決にもなりません。

調べたところ、二つの FontDescriptors をマージするようなメソッドはありませんでした。

と思っていたところ、大変有益な記事を見つけてしまった…

なんとカスケードフォントというものを利用することで「英語のときはこのフォント A、日本語のときはフォント B で表示したい」という要望を叶えることができると書いてある。

え、これ勝ったのでは???

UIFontDescriptor#

現段階ではまだCTFontDescriptorという型なのでこれを変換します。ただし、継承クラスなので無条件に成功します。

UIFontDescriptorはそのままUIFontに突っ込めて、UIFontはそのまま SwiftUI のFontに突っ込めるので逆順に追うと以下のような流れになるわけです。

11. URL -> as
10. CFURL -> CTFontManagerCreateFontDescriptorsFromURL()
9. CFArray? -> unwrap
8. CFArray -> as?
7. [CTFontDescriptor]? -> unwrap
6. [CTFontDescriptor] -> first
5. CTFontDescriptor? -> unwrap
4. CTFontDescriptor -> as
3. UIFontDescriptor -> init
2. UIFont (UIKit) -> init
1. Font (SwiftUI)

ということでアンラップも含めれば 11 段階遡ることで URL から SwiftUI で利用できる Font まで繋げられることがわかりました。

このときUIFontDescriptorに対して適切にカスケードフォントを設定することで一つのフォントファミリーで Splatfont1 と Splatfont2 に対応できるはずです。

では、そのコードを書いていきましょう。

SwiftUI での実装#

上の例ではスプラ 2 向けのフォントを利用していましたが、スプラ 3 では中国語と韓国語に対応しなければいけないので最初か r あらこちらで実装します。

フォント#

Splatoon1-common.3b7ce8b3c19f74921f51.woff2
Splatoon1-symbol-common.38ddb9a11cb1f225e092.woff2
Splatoon1-cjk-common.62441e2d3263b7141ca0.woff2
Splatoon1JP-hiragana-katakana.7650dccc9af86f19f094.woff2
Splatoon1JP-level1.fafc97f04a568e26ba52.woff2
Splatoon1JP-level2.225bb1db5962c9d61773.woff2
Splatoon1KRko-level1.a94dd3748648749f4583.woff2
Splatoon1KRko-level2.fcce77dce5655afed7d2.woff2
Splatoon1CHzh-level1.6b6af277c3dc45a8cf10.woff2
Splatoon1CHzh-level2.a24ca5d538d0b6a0d086.woff2
Splatoon1TWzh-level1.e991c1b3c2084df56d18.woff2
Splatoon1TWzh-level2.054b111fb7118a083ff7.woff2

フォントは全部で 12 種類で共通のフォントが 3 つあります。

言語フォント合計
Common4-
JP37
KO26
CH26
TW26

共通のフォントはプレイヤー名につけられる文字なので「記号・シンボル・ひらがな・かたかな・英語」です。

これらが含まれるのが上から四つのフォントファイルなのでこれらは必ず含む必要があります。なので日本語でも韓国語でも中国語でもなければフォントファイルは四つマージするだけで良いのですが、どうせなら日本語を突っ込めばいいので日本語とそれ以外で分けてしまうのが良いです。

enum Splatfont1: String, CaseIterable {
    case Splatoon1Common                = "3b7ce8b3c19f74921f51"
    case Splatoon1SymbolCommon          = "38ddb9a11cb1f225e092"
    case Splatoon1CjkCommon             = "62441e2d3263b7141ca0"
    case Splatoon1JPHiraganaKatakana    = "7650dccc9af86f19f094"
    case Splatoon1JPLevel1              = "fafc97f04a568e26ba52"
    case Splatoon1JPLevel2              = "225bb1db5962c9d61773"
    case Splatoon1KRkoLevel1            = "a94dd3748648749f4583"
    case Splatoon1KRkoLevel2            = "fcce77dce5655afed7d2"
    case Splatoon1CHzhLevel1            = "6b6af277c3dc45a8cf10"
    case Splatoon1CHzhLevel2            = "a24ca5d538d0b6a0d086"
    case Splatoon1TWzhLevel1            = "e991c1b3c2084df56d18"
    case Splatoon1TWzhLevel2            = "054b111fb7118a083ff7"
}

フォントはハッシュで区別できるのでしてしまいましょう。

これに対し、フォントをマージしてUIFontDescriptorとして返すメソッドを定義します。

UIFontを返してしまうとフォントのサイズの変更ができなくなるので注意

extension Splatfont1 {
    /// SplatNet3のURL
    var baseURL: URL {
        URL(string: "https://api.lp1.av5ja.srv.nintendo.net/static/media")
    }

    /// フォントのURL
    /// 必ず存在するので強制アンラップしても問題ない
    var fontURL: CFURL {
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
            .appendingPathComponent("static/media", conformingTo: .url)
            .appendingPathComponent(rawValue, conformingTo: .url)
            .appendingPathExtension("woff2") as CFURL
    }

    /// EnumからUIFontDescriptorを読み込む
    var fontDescriptor: UIFontDescriptor? {
        guard let array: CFArray = CTFontManagerCreateFontDescriptorsFromURL(self.fontURL),
              let fonts: [CTFontDescriptor] = array as? [CTFontDescriptor],
              let font: CTFontDescriptor = fonts.first
        else {
            return nil
        }
        return font as UIFontDescriptor
    }

    static let splatfont1jpja: UIFontDescriptor {
        [

        ]
    }
}
iOSアプリでのフォントの扱い方
https://fuwari.vercel.app/posts/2023/06/fonts/
Author
tkgling
Published at
2023-06-11