はじめに
Combineを使ってMVVMでNotificationCenterを実装するでも書きましたが、ありがたいことにiOS13以上の案件に携わりました。そこでは、新しくiOS13で追加されたフォント周りに関しても触れることができ、大変良い経験となりました。
また、それに付随してフォントの話で登壇する機会をいただきました。
(画像をクリックすると資料に飛べます)
内容はスライドをみていただくとわかりますが、iOS13周りで増えたフォントに関しての話をさせていただき、、
具体的なコードの話はあまり触れなかったので、こちらで実装面に関して触れていきたいと思います。
主に、フォント周りでできるようになったことを中心に記載していきます。
iOS13とフォント
iOS13からCTFontまわりのものがいくつか増えました。
この中のAPIを使ってフォント周りを操作していきます。
それでは実装コードを早速みていきましょう。
カスタムフォントをサクッと使う
資料でもこちらは触れているのでさらっと書いていきます。
UIFontPickerViewController
を使うことでAppleが用意してくれたフォントを簡単に使うことができます。
(以下、実際に作成された方の動画をお借りしています。)
選択したフォントが即座にラベルに反映されているのがみてわかります。
原理としてはとても単純で
Delegateを通りしてFontを取得できるので、そちらをセットしているだけです。
自前のカスタムフォントを使えるように設定する
フォントの扱うには、準備が必要なので先に行いましょう。
① フォントをアプリ内に用意する
今回は例として「NotoSans-Black」のフォントを使用します。
Assetにフォントのリソースを追加します。
その後、Resource Tagにも追加します。
上記で、追加したリソースタグ名を「Font」という名前を設定していますが、これは後々コードでも使用することになります。
(なので各自でユニークな名前を設定してください)
② フォントのentitlementsを追加する
これがないといくら実装してもエラーになります。
「Signin & Capabilities」から「+」で追加しましょう。
(Xcode11からUIが微妙に変わって探しづらいので注意)
「fonts」と検索すると該当するものが出てくるので追加します。
無事に追加したらチェックをつけましょう。
これで準備は完了です。
フォントのインストール / アンインストール
1. フォントをインストールする
いくつかの工程を挟むため段階的に説明していきます。
① リソースアクセスの確認
フォントをインストールするには、まずリソースにアクセスできるか確認する必要があります。
(しないとコードでエラーが出てしまうため)
アクセスできるかを確認するにはNSBundleResourceRequest
で確認します。
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になるとはずなのでこちらを通るはず)
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
を使用してインストールします。
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
で取り出す必要があります。
総括
①~③のコードを連携させたものです。
(単発のコード群だったので、ちゃんと機能するものを記載しておきます。)
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
}
}
}
実際の実行コード例は以下になります。
let fontList = ["NotoSans-Black"] as CFArray
let assetList: Set<String> = ["Font"]
requestFont(tags: assetList, fonts: fontList)
実際に設定画面で確認してみるとインストールされていることが確認できます。
この設定画面からアンインストールすることも可能ですが、次はコードからの実装をみてみましょう。
2. フォントをアンインストールする
アンインストール用の型が決まっているため、それを用意するためにStringのExtensionを用意しました。
CTFontDescriptor
の配列を用意する必要があるため、フォント名から取得できるようにStringから拡張できるようにしています。
(個人的な好みでExtensionにしたので、メソッドでもかまいません)
extension String {
var fontDescriptor: CTFontDescriptor? {
return (CTFontManagerCopyRegisteredFontDescriptors(.persistent, true) as? [CTFontDescriptor])?.first {
CTFontDescriptorCopyAttribute($0, kCTFontNameAttribute) as? String == self
}
}
}
CTFontManagerCopyRegisteredFontDescriptors
は後々の文章で説明しますが、行なっていることとしては、
「指定したフォント名がインストールされていればCTFontDescriptor
を返す」
というExtensionになっています。
では、実際のアンインストールコードをみていきます。
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
}
}
}
お気付きの方もいるかもしれませんが、先ほどのインストールメソッドとほとんど変わりません。
実際の実行コード例は以下になります。
let fontList = ["NotoSans-Black"]
let fontDescriptors = fontList.compactMap { $0.fontDescriptor }
uninstall(fontDescriptors: fontDescriptors)
先ほどと同様に設定画面からフォントがアンインストールされているか確認することができます。
フォントをアプリ内で使用する
1. フォントを参照する
CTFontManagerCopyRegisteredFontDescriptors
でそのアプリでいれたフォントを参照することができます。
ただし
「他のアプリや外部からインストールしたものは参照できない!!!」
ので注意してください。
例として「Noto Sans」の「NotoSans-Bold」をアプリからインストールしていた場合は以下のようになります。
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. フォントを使用する
取り出したUICTFontDescriptor
はUIFont
でそのまま使用できます。
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
の配列にして保持しておくのが良いかと思います。
先の自分が記載したコードでは1つのフォント(descriptors?.first
の部分)しか取りだしていませんが、このように「フォント名」をString
の配列で保持することで、必要なフォントを選択できるような実装が可能です。
CTFontDescriptorCopyAttribute
を使用することで CTFontDescriptor
からフォントの名前だけを取り出すことが可能です。
先のコードの延長として簡易なコードを記載しておきます。
// インストール済みフォント一覧取得
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側でフォントを消してくれるようになります。
実際に設定すると、その期限になった時にアラートがでるようになります。
設定するためには、info.plistに追加の記載が必要になります。
- URL Scheme
- FontProviderSubscriptionSupportInfo
それぞれで設定したschcmeを揃える必要があります。
この追加する「FontProviderSubscriptionSupportInfo」ですが、
公式に公開されているAPIではないので補完されてでてきません。
なので、plistにコードで直接追加してください。
設定項目としては
- 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