完全に動作保証された方法ではありません
敵「この画面の露出を上げたいので、タブ増やしてそこに配置できないですかね?」
ぼく「あー今全部で5タブ使ってしまっているので、これ以上は難しいですね」
ぼく「HIG(Human Interface Guidelines)上はタブは最大5個までなので」
ぼく( UITabbarController
でのタブ実装自体を変更するのは勘弁だなあ)
ぼく「最悪、審査でのリジェクトリスクもありますよ!」
敵「でも、(某有名アプリ)は6タブでストア公開してますよね?」
ぼく「 🙄 」
というわけで、 UITabBarController
でどうにか6タブ表示する方法を調べました。
前提情報
iOS アプリでタブ表示を実現するとなると、 UITabBarController
が最初の選択に入ってくるかと思います。
UITabBarController
は非常によくできており、HIG通りiPhone端末での6タブ以上の表示を防止するために、6タブ以上を設定すると省略表記とする動きが存在します。
これを省略せずに6タブを展開した状態で表示させたいというのが今回の目的です。
調査
HIG上、タブの最大数が5個というのは界隈では比較的通じる話ではあるのですが、実は iPad 上はこの限りではありません。
In general, use between three and five tabs on iPhone; if needed, a few more are acceptable on iPad.
実際、 iPad で UITabBarController
を利用して6タブ表示してみると、 Portrait な向きにおいても6タブ表示されます。
iPhone | iPad |
---|---|
iPhone と iPad で UITabBarController
の動作が変わってくるため、何かしらでの条件分岐があると思われ、実際に動かして調べたところ Size Class によって動作が変わっているようでした。
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
実際、 iPhone 端末では UITabBarController
による6タブ表示が難しいと思われているのですが、上の表の通り iPhone 11 で Landscapeの向きで表示してみると、何もせずとも6タブ表示できてしまいます。
Portrait | Landscape |
---|---|
対応方法
検証コード全体は以下を参照ください。
前述の調査の通り、 Size Classes によって6タブの場合に省略されているかどうかが制御されているようなので、強引にこれを書き換えに行きます。
具体的な対象としては、 UIViewController.traitCollection:.horizontalSizeClass
となります。
var horizontalSizeClass: UIUserInterfaceSizeClass { get }
UITraitEnvironment.traitCollection
UITraitCollection.horizontalSizeClass
ただし、このプロパティは直接値を設定することはできないため、親となる ViewController から以下のメソッドを利用して、対象の ViewController の traitCollection を上書きする形となってきます。
(このため、 UITabBarController
を直接 UIWindow.rootViewController
に設定している場合は、親VCを1枚挟む必要があります)
func setOverrideTraitCollection(_ collection: UITraitCollection?,
forChild childViewController: UIViewController)
実際のコードとしては、以下のような感じになるかと思います。
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
が引き継がれないような対応も入れておいた方が良さそうでした。
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 |
---|---|
1点問題があるのが Landscape 表示で、上述の表で
Compact width, compact height
となっている端末においては、 Landscape な向きの表示において、タブ上部がはみ出てしまいます。
5タブ | 6タブ |
---|---|
このため、
- Landscape はサポートしない
-
UITabBarController
は利用したい -
traitCollection
の書き換えによる不明な影響を考慮しても6タブ化したい
といった条件が揃って、初めて導入できる対応といった感じとなっています。