iOSのDynamic Typeについて

# Dynamic Type (ダイナミックタイプ)
Dynamic TypeはiOS上でデフォルトの文字の大きさを変更する機能です。対応しているアプリの文字サイズを一度に全部変えられます。
大きさだけでなくフォントの種類やスタイルも決められているので、Apple推奨の見た目を簡単に得られるというメリットがあります。その一方で、考慮しなければならない条件がひどく増えてしまう、厄介な機能でもあります。テストの手間を増やすだけでなく、画面設計にも大きく影響を及ぼします。
本文章は、Dynamic Typeに対する備忘録です。
対象OS：iOS9/iOS10/iOS11
対象Xcode：9.4

## Dynamic Typeを使うためのユーザー操作
設定アプリに２通り用意されています。

- その１：(iOS9 - iOS13)
    - [設定] -> \[画面表示と明るさ\] -> [文字サイズを変更]
- その２：(iOS9 - iOS12)
    - [設定] -> [一般] -> [アクセシビリティ] -> [さらに大きな文字]
- その２：(iOS13)
    - [設定] -> [アクセシビリティ] -> [さらに大きな文字]

その２では「さらに大きな文字」のスイッチをオンにすることができ、文字サイズの選択肢を大幅に増やすことができます。

## フォントの指定方法
### インターフェースビルダーで指定
フォントを選択する際、Text Stylesにカテゴライズされているものを選びます。
![XCodeのフォント選択の図](https://qiita-image-store.s3.amazonaws.com/0/137262/0018265e-7cc0-d835-7677-8d0d2f2f5c3a.png)


### コードでのフォント指定
`UIFont`クラスのクラスメソッド`preferredFont()`に"スタイル"を指定することで、設定に応じたフォントオブジェクトを得られます。

```swift
let font = UIFont.preferredFont(forTextStyle: UIFontTextStyle(rawValue: style))
```

## 動的な設定変更に追従するために
ダイナミックタイプは設定アプリからいつでもサイズが変えられます。
この変更に動的に追従するためには、もう一手間が必要です。

### iOS10以降、UILabel、UITextField、UITextViewは自動的に更新される
インターフェースビルダーで、Dynamic Typeの[Automatically Adjust Font]をチェックすると、表示される文字が自動的に更新されるようになります。
![xcodeSelectDynamicType_small.png](https://qiita-image-store.s3.amazonaws.com/0/137262/172b1547-c6be-41bb-92a5-9a1dd2b0f1e5.png)

コードでは、`UIContentSizeCategoryAdjusting`プロトコルの`adjustsFontForContentSizeCategory`を`true`にすることで同様に自動的に処理されるようになります。

```swift
hogeLabel.adjustsFontForContentSizeCategory = true
```

`UIContentSizeCategoryAdjusting`プロトコルに対応しているのは、`UILabel`、`UITextField`、`UITextView`だけです。
この指定によるフォントサイズ変更は、`viewWillLayoutSubviews()`と`viewDidLayoutSubviews()`の間でされるようです。
`UIContentSizeCategoryAdjusting`プロトコルに対応していない`UISegmentedControl`などは、iOS9以前と同様に、次の通知によって対応することになります。


### iOS8以降は、`traitCollectionDidChange`を使う。
`UITraitEnvironment`プロトコルの`traitCollectionDidChange`メソッドが`UIViewControler`や`UIView`で使えます。
iOS13のダークモードの変更にもこのメソッドで対応できます。

```swift
class MyViewController : UIViewController {
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        // UIFont.preferredFont() を使ってフォントを設定する。
        hoge1.font = UIFont.preferredFont(forTextStyle: .body)
        ...
    }
}
```

<Details>
<Summary>編集履歴：</Summary>
* 2020/2/10 `traitCollectionDidChange`について追記。
</Details>


### 通知を受けて自分で反映する場合。
Notificationを受け取って処理する場合は以下のようになります。
`UIContentSizeCategory.didChangeNotification`(swift 4.1までは`NSNotification.Name.UIContentSizeCategoryDidChange`でした)によって、Dynamic Typeの動的な変更をキャッチすることができます。これをUIに反映することで、動的なサイズ変更に追従することができます。

```swift
var anObserver:NSObjectProtocol?
override func viewWillAppear(_ animated: Bool) {
    anObserver = NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification /*NSNotification.Name.UIContentSizeCategoryDidChange*/, object: nil, queue: OperationQueue.main, using: updateFont)
}
override func viewWillDisappear(_ animated: Bool) {
    if anObserver != nil {
        NotificationCenter.default.removeObserver(anObserver!)
        anObserver = nil
    }
}
func updateFont(_ notification:NSNotification) {
    // UIFont.preferredFont() を使って全部再設定する。
    hoge1.font = UIFont.preferredFont(forTextStyle: .body)
    ...
}
```
<Details><Summary>編集履歴:</Summary>
* 2018/7/20: 上記コードにobserverの解除ができてないバグがあり、修正しました。なお、iOS9以降は`removeObserver`が不要、とAppleが言っていますが、それはビューコントローラーが解放される場合の話のようです。`viewWillAppear`でobserverを登録する場合、`viewWillDisappear`で`removeObserver`する必要があります。そうしないと、UITabBarControllerを使うなどして各コントローラーが解放されない場合、表示する度にobserverが追加されるため、一度の表示で何回も通知を受け取るようになります。
* 2019/3/6: 恥ずかしいミスを修正(obserber -> observer)
</Details>

### 通知での設定が優先される
通知の呼び出しは設定からアプリケーションに戻ったところで発生します。その後で、`viewWillLayoutSubviews()`や`viewDidLayoutSubviews()`が呼び出されます。しかし、通知を受け取ってフォントサイズを変更した場合、その後、`adjustsFontForContentSizeCategory = true`によるフォントサイズの自動変更が更に発生して設定を上書きすることはありません。通知による変更は、`adjustsFontForContentSizeCategory = true`よりも優先されるようです。
このため、表示崩れを防ぐために、通知の中で標準よりも小さい文字を指定して逃げるような使い方も可能ですし、インターフェースビルダーでのDynamic Typeのチェックも神経質にならずに使うことができます。

### iOS9.3シミュレーターのバグ(?)
なお、XCode9.4に付属のiOS9.3のシミュレーターには、動的なサイズ変更が正しく反映されない不具合があるようです。変更は反映されず、通知も発行されません。 文字サイズの設定を変更した後、Cmd+WでシミュレーターのiPhoneを一旦終了すると、次の起動では反映されるようです。
iOS10以降のシミュレーターでは動的な文字サイズの変更をテストできます。

### Xcode11のバグ
Xcode11で、ストーリーボードでダイナミックタイプフォントを選択できない場合があるようです。私の環境はXcode 11.3.1ですがこの現象が発生しています。仕方ないので全部コードでフォントを指定しています・・・
[Stack Overflowでの議論](https://stackoverflow.com/questions/58311562/how-to-assign-dynamic-text-styles-in-xcode-11)

## 設定されるフォントサイズ
### iOS9/10の場合

設定のフォントサイズを変更した場合の各pointSizeを表にまとめます。
1から7までが通常指定できる大きさで、デフォルトは4です。
8以降は「さらに大きな文字」スイッチをオンにすることで選べるようになります。

| スタイル | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|--:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|.body    |14|15|16|17|19|21|23|28|33|40|47|53|
|.callout |13|14|15|16|18|20|22|22|22|22|22|22|
|.caption1|11|11|11|12|14|16|18|18|18|18|18|18|
|.caption2|11|11|11|11|13|15|17|17|17|17|17|17|
|.footnote|12|12|12|13|15|17|19|19|19|19|19|19|
|.headline|14|15|16|17|19|21|23|23|23|23|23|23|
|.subheadline |12|13|14|15|17|19|21|21|21|21|21|21|
|.title1  |25|26|27|28|30|32|34|34|34|34|34|34|
|.title2  |19|20|21|22|24|26|28|28|28|28|28|28|
|.title3  |17|18|19|20|22|24|26|26|26|26|26|26|

iOS10までは、さらに大きな文字で大きくなるのは.bodyだけでした。

### iOS11の文字サイズ
iOS11ではスタイルが一つ増えただけでなく、個々の文字サイズも変わっています。(太字はiOS10までに対する変更点)

**iPhone5/SE系:**

| スタイル | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|--:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|.body       |14|15|16|17|19|21|23|28|33|40|47|53|
|.callout    |13|14|15|16|18|20|22|**26**|**32**|**38**|**44**|**51**|
|.caption1   |11|11|11|12|14|16|18|**22**|**26**|**32**|**37**|**43**|
|.caption2   |11|11|11|11|13|15|17|**20**|**24**|**29**|**34**|**40**|
|.footnote   |12|12|12|13|15|17|19|**23**|**27**|**33**|**38**|**44**|
|.headline   |14|15|16|17|19|21|23|**28**|**33**|**40**|**47**|**53**|
|.subheadline|12|13|14|15|17|19|21|**25**|**30**|**36**|**42**|**49**|
|.title1    |25|26|**26**|**26**|**27**|**28**|**30**|**36**|**41**|**46**|**51**|**56**|
|.title2    |19|20|**20**|**20**|**21**|**22**|**24**|**29**|**35**|**41**|**47**|**54**|
|.title3    |**19**|**20**|**20**|20|**21**|**22**|**24**|**29**|**35**|**41**|**47**|**54**|
|**.largeTitle**|**31**|**32**|**32**|**32**|**33**|**34**|**35**|**42**|**46**|**50**|**54**|**58**|

「さらに大きな文字」をオンにしたときの文字サイズが.body以外も大きくなっています。
（title2とtitle3が同じなのは奇妙ですが、実際そうなっています。）

実際のフォントで図にしてみるとこうなります。
左のタイトルはデフォルト設定での各スタイルの文字で、右側の数字が個々の設定に応じた実際の大きさです。
![capture.png](https://qiita-image-store.s3.amazonaws.com/0/137262/a598381a-5035-923c-9401-2305c2015274.png)

これだけ大きさが違うと簡単には対応できないですよね・・・

追記:2018/7/24 iOS11では、デバイス毎に異なるサイズが使われていることがわかったので以下を追加します。

**iPhone6系, 6Plus系, X:**

| スタイル    | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|--:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|.body       |14|15|16|17|19|21|23|28|33|40|47|53|
|.callout    |13|14|15|16|18|20|22|26|32|38|44|51|
|.caption1   |11|11|11|12|14|16|18|22|26|32|37|43|
|.caption2   |11|11|11|11|13|15|17|20|24|29|34|40|
|.footnote   |12|12|12|13|15|17|19|23|27|33|38|44|
|.headline   |14|15|16|17|19|21|23|28|33|40|47|53|
|.subheadline|12|13|14|15|17|19|21|25|30|36|42|49|
|.title1     |25|26|**27**|**28**|**30**|**32**|**34**|**38**|**43**|**48**|**53**|**58**|
|.title2     |19|20|**21**|**22**|**24**|**26**|**28**|**34**|**39**|**44**|**50**|**56**|
|.title3     |**17**|**18**|**19**|20|**22**|**24**|**26**|**31**|**37**|**43**|**49**|**55**|
|.largeTitle |31|32|**33**|**34**|**36**|**38**|**40**|**44**|**48**|**52**|**56**|**60**|

こちらも図にしてみました。
![iOS11FontSizesOnX.png](https://qiita-image-store.s3.amazonaws.com/0/137262/c84bba03-6f35-21e7-05d7-170f485f7d51.png)





#終わりに
Dynamic TypeはiOS7で導入され、iOS11でさらに大きな文字に対応するように拡張され、iOSのデフォルトアプリ全体でも対応が進みました。iOS13では設定アプリのアクセシビリティ項目を一つ上の階層に持ってきています。
Appleはもともとアクセシビリティに高い関心を寄せてきた会社ですが、社会的影響力が大きくなって、ますます熱意を注いでいるようです。特にiOS11では非常に大きな文字を指定できるようになりました。これは、Appleの弱視の人々に対する企業姿勢を表しているように思います。

残念ながら対応しているアプリは多くないようですが、こういうところにこそ、企業の姿勢が表れるように思います。特に小さい文字を見にくいと感じる人々にとっては、なくてはならない機能であり、高齢化の進んだ日本では多くのユーザーに影響を及ぼすようにも思います。
対応アプリが増えて、より多くの人にやさしい社会に近づくことを願います。