【iOS 11】Dynamic Type でカスタムフォントに対応する

ご覧いただきありがとうございます。
この記事は Qiita Advent Calendar 2017 ゆめみ[^0] の第 22 日目の記事です。
前日は @clown0082 さんの記事でした。

## はじめに
iOS 11 から Dynamic Type でシステムフォントに加えて
ついにカスタムフォントが設定できるようになりました。
本記事執筆時では iOS 11 だけが対応可能となっており，
なかなか採用されにくいとは思いますが，
この記事では，どのようにしたら対応できるのかについて書きます。

システムフォントの場合とカスタムフォントとで一部表示が異なることがあり
もしご存知の方がいらっしゃったら教えていただきたいです。

この記事は iOS の Advent Calendar 2017 第 21 日目の記事[^2]に
関連した内容になります。合わせてご覧いただけたら嬉しいです。


## Dynamic Type とは(おさらい)
Dynamic Type は iOS 7 から導入されました。
設定アプリ，iOS 11 からはコントロールセンターに追加することで
ユーザが好みのフォントサイズに変えられますよね。
ユーザが設定したフォントサイズに伴って
アプリ内のフォントサイズも変化させるというものです。

私は一番小さなフォントサイズ設定が好きです。
ただ，全ユーザがそうだとは限りません。
ユーザによってみやすいフォントサイズは異なるという意識を持つことが大事です。

## Dynamic Type でカスタムフォントに対応する

ひとつ前の記事[^2]に書いたのと同じ実装をした上でさらなる実装が必要です。
`UIFontTextStyle` を適用しただけではシステムフォントのままだからです。
下記が Dynamic Type 対応の要点(復習)です。

### Storyboard など IB の場合
* UI 部品を配置しフォント部分で `UIFontTextStyle` のどれかを選ぶ
* Automatically Adjusts Font 部分にチェックを入れる

### コードの場合
* UI 部品のインスタンスを作り，フォント設定に `UIFontTextStyle` を指定
*  `adjustsFontForContentSizeCategory`を `true` にする

```swift:swift
// body を設定する例
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true
```

### カスタムフォントの対応

カスタムフォントに対応する場合は上記の実装に加えて，
`UIFontMetrics`[^1] を用います。
カスタムフォントを `scaledFont(for:)` メソッドに渡すことで
ユーザが設定中のフォントサイズに合うように自動調整された
指定された `UIFontTextStyle` のフォントが得られます。
多分コードで書くしかないです。。。

`Futura` に最近ハマっているのでカスタムフォントの例として使います。
iOS 11 だけのコードでよければ下記のようになります。

```swift:swift
// Futura-Medium で body を設定する例
let customFont = UIFont(name: "Futura-Medium", size: UIFont.labelFontSize)
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true
```

iOS 11 未満はシステムフォントになるように場合分けします。
もっと綺麗に書けそうな気がします。

```swift:swift
if #available(iOS 11.0, *) {
    // カスタムフォントの綴り間違いには注意
    if let customFont = UIFont(name: "Futura-Medium", size: UIFont.labelFontSize) {
        label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont)
    } else {
        label.font = UIFont.preferredFont(forTextStyle: .body)
    }
} else {
    // iOS 11 未満はシステムフォントにする
    label.font = UIFont.preferredFont(forTextStyle: .body)
}
```

## 簡単な実装例で確認
### サンプルコード[^3]
GitHub にサンプルアプリを用意しましたので
見づらいところ等，適宜参考にしてください。
Swift は 今まで業務で携われていないこともあり，
まともにレビューしてもらったことがないので
コード間違い・改善点等のご指摘，是非お願いいたします。

* [サンプルコード(GitHub)](https://github.com/MilanistaDev/DynamicTypeSample)

### 実装環境
* Xcode 9.2
* iOS 10 and later(実質 iOS 11 以上)
* 説明の都合上 FatViewController でお送りします

### 実装例
例として下記のような文字列と `UIFontTextStyle` の Array を用意し，
`TableView` のセルに流し込んで `textLabel.text` に表示させてみます。

```Swift:Constant.swift
struct DynamicTypeSample {
    static let textArray: [String] =
        ["Body", "Callout", "Caption1", "Caption2", "Footnote",
         "Headline", "Subhead", "Title1", "Title2", "Title3"]
    static let styleArray: [UIFontTextStyle] =
        [.body, .callout, .caption1, .caption2, .footnote,
         .headline, .subheadline, .title1, .title2, .title3]
}
```

`UITabelViewDataSource` の実装だけ書くと下記のようになります。

<details><summary>**実装例：クリックでコード表示**</summary><div>

```swift:CustomFontViewController.swift
extension CustomFontViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return DynamicTypeSample.textArray.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "dynamicTypeCustomFontListCell")
        cell?.textLabel?.text = DynamicTypeSample.textArray[indexPath.row]
        // Custom Font は iOS 11 から
        if #available(iOS 11.0, *) {
            if let customFont = UIFont(name: "Futura-Medium", size: UIFont.labelFontSize) {
                cell?.textLabel?.font = UIFontMetrics(forTextStyle: DynamicTypeSample.styleArray[indexPath.row]).scaledFont(for: customFont)
            } else {
                cell?.textLabel?.font = UIFont.preferredFont(forTextStyle: DynamicTypeSample.styleArray[indexPath.row])
            }
        } else {
            cell?.textLabel?.font = UIFont.preferredFont(forTextStyle: DynamicTypeSample.styleArray[indexPath.row])
        }
        cell?.textLabel?.adjustsFontForContentSizeCategory = true
        cell?.selectionStyle = .none
        return cell!
    }
}
```
</div></details>

セクション(Header)がある場合は文字列を流し込むだけでは
システムフォントのままになるのでカスタムフォントに対応するために
`UITableViewDelegate` の Header 系の Delegate メソッドで個別で書く必要があります。

<details><summary>**実装例：クリックでコード表示**</summary><div>

```swift:CustomFontViewController.swift
extension CustomFontViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let headerView = self.generateHeaderView()
        return headerView
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        let headerView = self.generateHeaderView()
        return headerView.frame.size.height
    }
}

// Header の UIView を生成し返す
func generateHeaderView() -> UIView {
    let sectionLabel = UILabel(frame: CGRect(x: self.dynamicTypeListTableView.separatorInset.left,
                                             y: 4.0,
                                             width: (self.dynamicTypeListTableView.frame.size.width - self.dynamicTypeListTableView.separatorInset.left),
                                             height: 0.0))
    sectionLabel.text = "Custom Font"
    if #available(iOS 11.0, *) {
        if let customFont = UIFont(name: "Futura-Medium", size: UIFont.labelFontSize) {
            sectionLabel.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: customFont)
        } else {
            sectionLabel.font = UIFont.preferredFont(forTextStyle: .headline)
        }
    } else {
        sectionLabel.font = UIFont.preferredFont(forTextStyle: .headline)
    }
    sectionLabel.sizeToFit()
    let sectionView = UIView(frame: CGRect(origin: .zero,
                                           size: CGSize(width: self.dynamicTypeListTableView.frame.size.width,
                                                        height: sectionLabel.frame.size.height + 8.0)))
    sectionView.backgroundColor = UIColor(red: 232/255, green: 233/255, blue: 237/255, alpha: 1)
    sectionView.addSubview(sectionLabel)

    return sectionView
}
```
</div></details>

### 実行例
実行し，`Accessibility Inspector` を使って
フォントサイズを変えてみると下記のような感じになります。
`Accessibility Inspector` の使い方についてはこの記事[^2]をご覧ください。

![dynamictype_exe_02.gif](https://qiita-image-store.s3.amazonaws.com/0/88266/52a6f2fb-6136-84b6-1b33-972d334dfcbb.gif "dynamictype_exe_02.gif")

各 `UIFontTextStyle` ごとにちゃんと変化していることがわかります🎉


がしかし，システムフォント側となんか違う・・・
特に `Caption2`... 明らかに大きい。
🤔

<img width="899" alt="dyna_01.png" src="https://qiita-image-store.s3.amazonaws.com/0/88266/7f5c697b-68da-98ca-7e65-32fd5c7c2520.png">


## システムフォントとの挙動の違いについて
この動作だけ見ると問題なくフォントサイズが変わっているように
見えるのですが，一部の `UIFontTextStyle` で大きさが
システムフォントと異なってしまっています。

このことについて調べてみたのですがよくわからずでした。
下記の記事にもこのことが一部触れられていて `.caption2` の代わりに
`.largeTitle` (iOS 11からの `UIFontTextStyle`)が使われているようで
バグではないかということでした。

> Note: I am not sure if it is a bug or a “feature” but the .caption2 style seems to scale larger than the .caption1 style even though it uses a smaller point size at the .large size.

via
[Using A Custom Font With Dynamic Type](https://useyourloaf.com/blog/using-a-custom-font-with-dynamic-type/)

とりあえず，Technical Support Incident(TSI)[^4]で質問してみようと思います。
こういうのを WWDC のラボで聞けばいいんだろうなー

## おわりに
iOS 11 から Dynamic Type でカスタムフォントを指定できるように
なったので今回はその対応方法について記事を書きました。
ありそうでなかった機能なので WWDC 17 のときは嬉しかったのですが，
iOS 11 以上かそれ未満かで場合分けなどいざ実装してみると色々大変で
iOS 10 のサポート切ってもいいかなというタイミング(iOS 13くらい？)まで
本格的な採用はないのでは？というのが現時点の私見です。

Dynamic Type 自体が商用アプリとして採用されにくい部分があるので，
まずは社内ハッカソンや社内の便利ツール(受付アプリなど)など
小規模な単位で対応してみたら面白いかもしれないですね！
Apple が WWDC 17 の色々なセッションで言及しているだけに
知見はためておいて損はない気がします。

来年も引き続き UI 周りに力入れて，新しい技術習得をしながら，
日々の業務・個人アプリの制作を頑張っていこうと思います。
ここまでご覧いただきありがとうございました！

明日は @u-minor さんが担当します！！


## 参考
* [Getting a Scaled Font](https://developer.apple.com/documentation/uikit/text_display_and_fonts/getting_a_scaled_font)
* [Using A Custom Font With Dynamic Type](https://useyourloaf.com/blog/using-a-custom-font-with-dynamic-type/)
* [labelFontSize](https://developer.apple.com/documentation/uikit/uifont/1623398-labelfontsize)

## 引用
[^0]: [Qiita Advent Calendar 2017 ゆめみ](https://qiita.com/advent-calendar/2017/yumemi)
[^1]: [UIFontMetrics](https://developer.apple.com/documentation/uikit/uifontmetrics)
[^2]: [Dynamic Type 対応について考える](https://qiita.com/MilanistaDev/items/cda50cde2993293ad6c8)
[^3]: [サンプルコード(GitHub)](https://github.com/MilanistaDev/DynamicTypeSample)
[^4]: [Technical Support Incident(TSI)](https://developer.apple.com/support/technical/jp/)