どうしても UITabBarController で6タブ表示したい

:::note warn
完全に動作保証された方法ではありません
:::

敵「この画面の露出を上げたいので、タブ増やしてそこに配置できないですかね？」
ぼく「あー今全部で5タブ使ってしまっているので、これ以上は難しいですね」
ぼく「HIG（Human Interface Guidelines）上はタブは最大5個までなので」
ぼく（ `UITabbarController` でのタブ実装自体を変更するのは勘弁だなあ）
ぼく「最悪、審査でのリジェクトリスクもありますよ！」
敵「でも、（某有名アプリ）は6タブでストア公開してますよね？」
ぼく「 🙄 」

というわけで、 `UITabBarController` でどうにか6タブ表示する方法を調べました。

## 前提情報

iOS アプリでタブ表示を実現するとなると、 `UITabBarController` が最初の選択に入ってくるかと思います。

`UITabBarController` は非常によくできており、HIG通りiPhone端末での6タブ以上の表示を防止するために、6タブ以上を設定すると省略表記とする動きが存在します。

https://dev.classmethod.jp/articles/uitabbarcontroller_more_tab/

これを省略せずに6タブを展開した状態で表示させたいというのが今回の目的です。

## 調査

HIG上、タブの最大数が5個というのは界隈では比較的通じる話ではあるのですが、実は　iPad 上はこの限りではありません。

>In general, use between three and five tabs on iPhone; if needed, a few more are acceptable on iPad.

https://developer.apple.com/design/human-interface-guidelines/ios/bars/tab-bars/

実際、 iPad で `UITabBarController` を利用して6タブ表示してみると、 Portrait な向きにおいても6タブ表示されます。

|iPhone|iPad|
|-|-|
|<img width="300" alt="iPhone.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/aa6155d7-3653-9005-1508-2e3a73279619.png">|<img width="500" alt="iPad.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/bbd9e699-6421-6d13-10a7-562aecc61052.png">|

iPhone と　iPad で `UITabBarController` の動作が変わってくるため、何かしらでの条件分岐があると思われ、実際に動かして調べたところ [Size Class](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout#size-classes) によって動作が変わっているようでした。

Size Classes の詳細についてはググっていただくとして、ざっくり言ってしまうと、 Size Classes は画面の幅と高さを

- `compact`
- `regular`

の2つのカテゴリに分類したものとなっています。

iPad での Multitasking による画面分割を無視すると、端末毎の設定は以下の表となるのですが、 `UITabBarController` が省略せずに6タブ表示する場合が、この表の _Regular width_ 部分に一致してきます。

|Device|Portrait orientation|Landscape orientation|
|-|-|-|
|12.9" iPad Pro|Regular width, regular height|Regular width, regular height|
|11" iPad Pro|Regular width, regular height|Regular width, regular height|
|10.5" iPad Pro|Regular width, regular height|Regular width, regular height|
|9.7" iPad|Regular width, regular height|Regular width, regular height|
|7.9" iPad mini|Regular width, regular height|Regular width, regular height|
|iPhone 12 Pro Max|Compact width, regular height|Regular width, compact height|
|iPhone 12 Pro|Compact width, regular height|Compact width, compact height|
|iPhone 12|Compact width, regular height|Compact width, compact height|
|iPhone 12 mini|Compact width, regular height|Compact width, compact height|
|iPhone 11 Pro Max|Compact width, regular height|Regular width, compact height|
|iPhone 11 Pro|Compact width, regular height|Compact width, compact height|
|iPhone 11|Compact width, regular height|Regular width, compact height|
|iPhone XS Max|Compact width, regular height|Regular width, compact height|
|iPhone XS|Compact width, regular height|Compact width, compact height|
|iPhone XR|Compact width, regular height|Regular width, compact height|
|iPhone X|Compact width, regular height|Compact width, compact height|
|iPhone 8 Plus|Compact width, regular height|Regular width, compact height|
|iPhone 8|Compact width, regular height|Compact width, compact height|
|iPhone 7 Plus|Compact width, regular height|Regular width, compact height|
|iPhone 7|Compact width, regular height|Compact width, compact height|
|iPhone 6s Plus|Compact width, regular height|Regular width, compact height|
|iPhone 6s|Compact width, regular height|Compact width, compact height|
|iPhone SE|Compact width, regular height|Compact width, compact height|
|iPod touch 5th generation and later|Compact width, regular height|Compact width, compact height|

_参照元：[Human Interface Guidelines / iOS / Adaptivity and Layout / Size Class](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout#size-classes)_

実際、 iPhone 端末では `UITabBarController` による6タブ表示が難しいと思われているのですが、上の表の通り iPhone 11 で Landscapeの向きで表示してみると、何もせずとも6タブ表示できてしまいます。

|Portrait|Landscape|
|-|-|
|<img width="300" alt="Portrait.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/a57cd1d8-cb10-b2b1-2b19-4887fbcbccee.png">|<img height="350" alt="Landscape.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/621d758c-1bf8-e1ec-6c42-c7904c373ab4.png">|

## 対応方法

検証コード全体は以下を参照ください。

https://github.com/hugehoge/example-six-tab-controller

前述の調査の通り、 Size Classes によって6タブの場合に省略されているかどうかが制御されているようなので、強引にこれを書き換えに行きます。

具体的な対象としては、 `UIViewController.traitCollection:.horizontalSizeClass` となります。

```swift
var horizontalSizeClass: UIUserInterfaceSizeClass { get }
```

__UITraitEnvironment.traitCollection__

https://developer.apple.com/documentation/uikit/uitraitenvironment/1623514-traitcollection

__UITraitCollection.horizontalSizeClass__

https://developer.apple.com/documentation/uikit/uitraitcollection/1623508-horizontalsizeclass

ただし、このプロパティは直接値を設定することはできないため、親となる ViewController から以下のメソッドを利用して、対象の ViewController の traitCollection を上書きする形となってきます。
（このため、 `UITabBarController` を直接 `UIWindow.rootViewController` に設定している場合は、親VCを1枚挟む必要があります）

```swift
func setOverrideTraitCollection(_ collection: UITraitCollection?, 
                       forChild childViewController: UIViewController)
```

https://developer.apple.com/documentation/uikit/uiviewcontroller/1621406-setoverridetraitcollection

実際のコードとしては、以下のような感じになるかと思います。

```swift:UITabBarController&#x20;のtraitCollection&#x20;を上書き
class RootViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    let firstVC = TabBarController()

    // `horizantalSizeClass = .regular`となるよう、`traitCollection` の設定を親 VC から上書きする
    setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: .regular),
                               forChild: firstVC)

    addChild(firstVC)
    view.addSubview(firstVC.view)
    firstVC.didMove(toParent: self)
  }
}

```

基本的にはこれで iPhone 端末においても6タブ表示が可能です。

ただ、`traitCollection` は親から子に引き継がれていくため、 `UITabBarController` から各タブのコンテンツとなる ViewController に対して、強制設定された `traitCollection` が引き継がれないような対応も入れておいた方が良さそうでした。

```swift:UITabBarController&#x20;の子&#x20;VC&#x20;のtraitCollectionを本来のものに戻す
class TabBarController: UITabBarController {
  private let contentViewControllers: [UIViewController] = [
    // 各タブのコンテンツとなる ViewController
  ]

  private let originalTraitCollection: UITraitCollection

  init(originalTraitCollection: UITraitCollection) {
    self.originalTraitCollection = originalTraitCollection

    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    for vc in contentViewControllers {
      // 本来の `traitCollection` に戻すように上書きする
      setOverrideTraitCollection(originalTraitCollection, forChild: contentVC)
    }
    setViewControllers(contentVCs, animated: false)
  }
}
```

## 対応後

今回の対応によって、以下のように iPhone 端末における Portrait な向きの表示において、6タブ表示が可能となってきます。
（一応、 iOS 13.X と 14.X で確認しています）

|iPhone SE 1st gen|iPhone 12 Pro Max|
|-|-|
|<img width="320" alt="iPhone_SE_1st_gen.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/bef7bd19-76ee-5705-2371-e2f8d9a32c8d.png">|<img width="428" alt="iPhone_12_Pro_Max.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/9ffd1dd8-262b-95f0-7be0-e7b3b81ee857.png">|

1点問題があるのが Landscape 表示で、上述の表で

>Compact width, compact height

となっている端末においては、 Landscape な向きの表示において、タブ上部がはみ出てしまいます。

|5タブ|6タブ|
|-|-|
|<img height="390" alt="5tabs.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/bf82da01-2c66-6fba-42c0-d5e4552e4b1f.png">|<img height="390" alt="6tabs.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/b4222873-07b9-93a8-fa06-fb32f6c73c06.png">|

このため、

- Landscape はサポートしない
- `UITabBarController` は利用したい
- `traitCollection` の書き換えによる不明な影響を考慮しても6タブ化したい

といった条件が揃って、初めて導入できる対応といった感じとなっています。