65
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

iOSAdvent Calendar 2019

Day 21

[Swift] iOS13とフォントのお話

Last updated at Posted at 2019-12-22

はじめに

Combineを使ってMVVMでNotificationCenterを実装するでも書きましたが、ありがたいことにiOS13以上の案件に携わりました。そこでは、新しくiOS13で追加されたフォント周りに関しても触れることができ、大変良い経験となりました。

また、それに付随してフォントの話で登壇する機会をいただきました。
(画像をクリックすると資料に飛べます)

thumbnail

内容はスライドをみていただくとわかりますが、iOS13周りで増えたフォントに関しての話をさせていただき、、
具体的なコードの話はあまり触れなかったので、こちらで実装面に関して触れていきたいと思います。

主に、フォント周りでできるようになったことを中心に記載していきます。

iOS13とフォント

iOS13からCTFontまわりのものがいくつか増えました。

スクリーンショット 2019-12-23 4.57.03.png

この中のAPIを使ってフォント周りを操作していきます。
それでは実装コードを早速みていきましょう。

カスタムフォントをサクッと使う

資料でもこちらは触れているのでさらっと書いていきます。
UIFontPickerViewControllerを使うことでAppleが用意してくれたフォントを簡単に使うことができます。
(以下、実際に作成された方の動画をお借りしています。)

UIFontPickerViewController.gif

選択したフォントが即座にラベルに反映されているのがみてわかります。

原理としてはとても単純で

スクリーンショット 2019-12-23 6.56.36.png

Delegateを通りしてFontを取得できるので、そちらをセットしているだけです。

自前のカスタムフォントを使えるように設定する

フォントの扱うには、準備が必要なので先に行いましょう。

① フォントをアプリ内に用意する

今回は例として「NotoSans-Black」のフォントを使用します。

Assetにフォントのリソースを追加します。

スクリーンショット 2019-12-23 3.31.10.png

その後、Resource Tagにも追加します。

スクリーンショット 2019-12-23 3.31.00.png

上記で、追加したリソースタグ名を「Font」という名前を設定していますが、これは後々コードでも使用することになります。
(なので各自でユニークな名前を設定してください)

② フォントのentitlementsを追加する

これがないといくら実装してもエラーになります。

「Signin & Capabilities」から「+」で追加しましょう。
(Xcode11からUIが微妙に変わって探しづらいので注意)

スクリーンショット 2019-12-23 5.04.00.png

「fonts」と検索すると該当するものが出てくるので追加します。

スクリーンショット 2019-12-23 4.03.42.png

無事に追加したらチェックをつけましょう。

スクリーンショット 2019-12-23 4.03.53.png

これで準備は完了です。

フォントのインストール / アンインストール

1. フォントをインストールする

いくつかの工程を挟むため段階的に説明していきます。

① リソースアクセスの確認

フォントをインストールするには、まずリソースにアクセスできるか確認する必要があります。
(しないとコードでエラーが出てしまうため)

アクセスできるかを確認するにはNSBundleResourceRequestで確認します。

.swift

private var resourceRequest: NSBundleResourceRequest?

func requestFont(tags: Set<String>, fonts: CFArray) {
    resourceRequest = NSBundleResourceRequest(tags: tags)
    resourceRequest?.conditionallyBeginAccessingResources { [weak self] isAvailable in
        if isAvailable {
            debugPrint("is available")
        } else {
            debugPrint("is not available")
        }
    }
}

1つ目の引数tagsは先ほど追加したリソースタグ名になります。
2つ目の引数fontsはインストールしたいフォント名を引数として渡します。

② リソースにアクセスする

アクセスができない場合は、アクセスできるようにします。
(①の処理は初回でfalseになるとはずなのでこちらを通るはず)

.swift
func accessFont(fonts: CFArray) {
    resourceRequest?.beginAccessingResources { [weak self] error in
        if error == nil {
            debugPrint("success")
        } else {
            debugPrint("failure", error?.localizedDescription ?? "")
        }
        self?.resourceRequest?.endAccessingResources()
    }
}

③ インストールする

iOS13から追加されたCTFontManagerRegisterFontsWithAssetNamesを使用してインストールします。

.swift
func installFont(fonts: CFArray) {
    CTFontManagerRegisterFontsWithAssetNames(fonts, CFBundleGetMainBundle(), .persistent, true) { errors, _ -> Bool in
        if 1 <= CFArrayGetCount(errors) {
            debugPrint("font install failure: \(unsafeBitCast(CFArrayGetValueAtIndex(errors, 0), to: CFError.self).localizedDescription)")
            return false
        } else {
            debugPrint("font install success")
            return true
        }
    }
}

引数にはインストールしたいフォント名を引数として渡します。
エラーをみたい場合はunsafeBitCastで取り出す必要があります。

総括

①~③のコードを連携させたものです。
(単発のコード群だったので、ちゃんと機能するものを記載しておきます。)

.swift
private var resourceRequest: NSBundleResourceRequest?

func requestFont(tags: Set<String>, fonts: CFArray) {
    resourceRequest = NSBundleResourceRequest(tags: tags)
    resourceRequest?.conditionallyBeginAccessingResources { [weak self] isAvailable in
        if isAvailable {
            debugPrint("is available")
            self?.installFont(fonts: fonts)
        } else {
            debugPrint("is not available")
            self?.accessFont(fonts: fonts)
        }
    }
}

func accessFont(fonts: CFArray) {
    resourceRequest?.beginAccessingResources { [weak self] error in
        if error == nil {
            self?.installFont(fonts: fonts)
        } else {
            debugPrint("failure", error?.localizedDescription ?? "")
        }
        self?.resourceRequest?.endAccessingResources()
    }
}

func installFont(fonts: CFArray) {
    CTFontManagerRegisterFontsWithAssetNames(fonts, CFBundleGetMainBundle(), .persistent, true) { errors, _ -> Bool in
        if 1 <= CFArrayGetCount(errors) {
            debugPrint("font install failure: \(unsafeBitCast(CFArrayGetValueAtIndex(errors, 0), to: CFError.self).localizedDescription)")
            return false
        } else {
            debugPrint("font install success")
            return true
        }
    }
}

実際の実行コード例は以下になります。

.swift
let fontList = ["NotoSans-Black"] as CFArray
let assetList: Set<String> = ["Font"]
requestFont(tags: assetList, fonts: fontList)

実際に設定画面で確認してみるとインストールされていることが確認できます。

スクリーンショット 2019-12-23 4.33.17 2.png

この設定画面からアンインストールすることも可能ですが、次はコードからの実装をみてみましょう。

2. フォントをアンインストールする

アンインストール用の型が決まっているため、それを用意するためにStringのExtensionを用意しました。
CTFontDescriptorの配列を用意する必要があるため、フォント名から取得できるようにStringから拡張できるようにしています。
(個人的な好みでExtensionにしたので、メソッドでもかまいません)

.swift
extension String {
    
    var fontDescriptor: CTFontDescriptor? {
        return (CTFontManagerCopyRegisteredFontDescriptors(.persistent, true) as? [CTFontDescriptor])?.first {
            CTFontDescriptorCopyAttribute($0, kCTFontNameAttribute) as? String == self
        }
    }
}

CTFontManagerCopyRegisteredFontDescriptors は後々の文章で説明しますが、行なっていることとしては、
「指定したフォント名がインストールされていればCTFontDescriptorを返す」
というExtensionになっています。

では、実際のアンインストールコードをみていきます。

.swift
func uninstall(fontDescriptors: [CTFontDescriptor]) {
    CTFontManagerUnregisterFontDescriptors(fontDescriptors as CFArray, .persistent) { errors, _ -> Bool in
        if 1 <= CFArrayGetCount(errors) {
            debugPrint("font uninstall failure: \(unsafeBitCast(CFArrayGetValueAtIndex(errors, 0), to: CFError.self).localizedDescription)")
            return false
        } else {
            debugPrint("font uninstall success")
            return true
        }
    }
}

お気付きの方もいるかもしれませんが、先ほどのインストールメソッドとほとんど変わりません。
実際の実行コード例は以下になります。

.swift
let fontList = ["NotoSans-Black"]
let fontDescriptors = fontList.compactMap { $0.fontDescriptor }
uninstall(fontDescriptors: fontDescriptors)

先ほどと同様に設定画面からフォントがアンインストールされているか確認することができます。

フォントをアプリ内で使用する

1. フォントを参照する

CTFontManagerCopyRegisteredFontDescriptors
でそのアプリでいれたフォントを参照することができます。

ただし
他のアプリや外部からインストールしたものは参照できない!!!
ので注意してください。

例として「Noto Sans」の「NotoSans-Bold」をアプリからインストールしていた場合は以下のようになります。

.swift
let descriptors = CTFontManagerCopyRegisteredFontDescriptors(.persistent, true) as? [CTFontDescriptor]

print(descriptors ?? [])

printの内容は以下

[UICTFontDescriptor <0x6000026827c0> = {
    NSCTFontFileURLAttribute = "file:///Users/XXXXXX/Library/Developer/CoreSimulator/Devices/60946E20-27DB-42F6-BAA1-35ABB6308F7B/data/Containers/Data/Application/E6695D3B-D062-4C35-9AEE-48DC6F50CD03/tmp/NotoSans-Bold-C91C8631-7E83-45F7-AD21-EC0502A772C5";
    NSFontFamilyAttribute = "Noto Sans";
    NSFontNameAttribute = "NotoSans-Bold";
}]

// ※ XXXXXXはユーザー名

このようにインストール済みのフォント情報を配列で参照することができます。

2. フォントを使用する

取り出したUICTFontDescriptorUIFontでそのまま使用できます。

.swift
let descriptors = CTFontManagerCopyRegisteredFontDescriptors(.persistent, true) as? [CTFontDescriptor]

/// 今回はとりあえず1つなので先頭のものを取り出しています。
guard let notoSansBoldDescriptor = descriptors?.first else { return }

label.font = UIFont(descriptor: notoSansBoldDescriptor, size: 30.0)

実際のところフォント情報の配列で返ってくるので、WWDCの動画にもあった「フォント名」をStringの配列にして保持しておくのが良いかと思います。

(以下動画の抜粋)
スクリーンショット 2019-11-04 14.26.17.png

先の自分が記載したコードでは1つのフォント(descriptors?.firstの部分)しか取りだしていませんが、このように「フォント名」をStringの配列で保持することで、必要なフォントを選択できるような実装が可能です。

CTFontDescriptorCopyAttribute を使用することで CTFontDescriptorからフォントの名前だけを取り出すことが可能です。

先のコードの延長として簡易なコードを記載しておきます。

.swift
// インストール済みフォント一覧取得
let descriptors = CTFontManagerCopyRegisteredFontDescriptors(.persistent, true) as? [CTFontDescriptor]

// Descriptor の取得
guard let notoSansBoldDescriptor = descriptors?.first else { return }

// Font名 の取得
guard let notoSansBold = CTFontDescriptorCopyAttribute(notoSansBoldDescriptor, kCTFontNameAttribute) as? String else { return }
        
label.font = UIFont(name: notoSansBold, size: 30.0)

フォントに使用期限を設ける

主にサブスクリプションなどを行う際に必要になるかと思います。
設定することでOS側でフォントを消してくれるようになります。

実際に設定すると、その期限になった時にアラートがでるようになります。

スクリーンショット 2019-12-23 6.27.47.png

設定するためには、info.plistに追加の記載が必要になります。

- URL Scheme
- FontProviderSubscriptionSupportInfo

それぞれで設定したschcmeを揃える必要があります。

スクリーンショット 2020-01-22 1.55.14.png

この追加する「FontProviderSubscriptionSupportInfo」ですが、
公式に公開されているAPIではないので補完されてでてきません
なので、plistにコードで直接追加してください。

スクリーンショット 2020-01-22 2.02.41.png

設定項目としては

- warn: 警告を表示するまでの日数
- expire: フォントを削除するまでの日数
- url: openを押した際のscheme設定
- test: テストモードのon/off

となっており、この日数はフォントをインストールしてから換算されます。

warnで設定したアラートを無視し続けると、いずれexpireで設定した日にアラートが出てフォントは自動で削除されます。
testをYesにすると、warn/expireで設定した1日が1分換算になり、すぐテストできるようになります。

*シミュレータのみ
*きっかり1分というわけではなく、例えば「5」にしても5-10分くらいかかったりするので注意

終わりに

iOS13ではSwiftUIやCombineに目がいきがちですが、、、
こういったデザイン周りに関わってくるアップデートもたくさんあり、着目してみると面白いものです。

まだ、情報としてあまり出回っていないので、面白い知見があれば是非お教え願いたいです!

サクッと動作するものを置いておきます
Github: Font_Install_Demo

65
47
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
65
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?