はじめに
SwiftUIのHostingViewA
からUIKitのViewControllerB
(縦画面固定)をモーダルで表示すると、横向き(landscape
)で表示→閉じた際に、元のSwiftUIのナビゲーションバーの高さが異常に大きくなる問題が起きます。
今回は、先生と生徒の会話を通してわかりやすく解説していきます。
登場人物
- 先生:アプリ開発に詳しい高校の情報科の先生。
- まさと:iOSアプリ開発を始めたばかりの高校2年生。
- あかね:UIまわりが気になるデザイナー志望の高校生。
ある日の放課後、情報教室にて
まさと:先生ー!SwiftUIからUIKitの画面(ViewControllerB)を呼び出すアプリ作ってるんですけど、へんなバグが出ました!
先生:どんなバグかな?
まさと:ViewControllerBを横向きで表示してから閉じると、SwiftUI画面に戻ったときナビゲーションバーがめっちゃでかくなるんです……
あかね:それってなんか見た目が壊れてるみたいでイヤだね〜!
先生:うん、それはよくある落とし穴なんだ。
UIKitとSwiftUIを組み合わせて使うと、向き(portrait/landscape)やサイズクラスがちゃんと戻らないことがある。
問題のコード
まさと:ViewControllerBでは、こうやって画面の向きを固定してます。
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
先生:なるほど。これでViewControllerBは縦画面固定だね。
でも、問題はここから。
横向きの状態でViewControllerBを出して、閉じると、元のSwiftUIの画面(HostingViewA)が「まだ横向きっぽい状態のまま」になるんだ。
あかね:へぇ〜。なんでそんなことに?
先生:iOSは画面の向きに関する情報を traitCollection
や Safe Area
で管理してる。
だけど、途中で強制的に縦固定の画面を出すと、それが閉じられたときに元の状態にうまく戻らないことがある。
解決方法を教えて!
まさと:じゃあ、どうすればナビゲーションバーが元に戻るの?
先生:それはね、「アプリ全体の向き制御の窓口」を1箇所にまとめて、今一番前面にいる画面の向きを見るようにするんだ。
あかね:え、アプリ全体のって、どこに書けばいいの?
先生:AppDelegate
っていうところに書くといいよ。
iOSアプリの「司令塔」みたいな役割だね。
AppDelegate に書くコード
func application(_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let topVC = UIApplication.topViewController() {
return topVC.supportedInterfaceOrientations
}
return .allButUpsideDown
}
まさと:おお!シンプル!
先生:これで、今一番上に表示されてる画面の「向きの設定」が、そのままアプリ全体の向き制御として機能するようになる。
トップViewControllerを取得する拡張
あかね:でも、一番上に表示されてるViewControllerって、どうやって取るの?
先生:いい質問だね。こんな便利な拡張があるよ:
extension UIApplication {
static func topViewController(
base: UIViewController? = UIApplication.shared
.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.first?.rootViewController
) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController,
let sel = tab.selectedViewController {
return topViewController(base: sel)
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
最後に確認しよう!
まさと:じゃあ、ViewControllerBではもう supportedInterfaceOrientations
は書かなくていいの?
先生:基本的には、NavigationControllerB でまとめて制御するのがスマートだね。
class NavigationControllerB: UINavigationController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override var shouldAutorotate: Bool {
return true
}
}
あかね:これで、デザインも崩れないし安心!
先生:うん。これで横向き→縦向きの切り替えもうまくいって、ナビバーの高さも正常に戻るようになるよ。
まとめ
- UIKit画面で向きを個別に固定すると、SwiftUI画面に戻ったときにサイズ情報が崩れることがある
- AppDelegate で「今一番上に表示されているVCの向き」を返すようにすると、向き切り替えが安定
- トップViewController取得には便利な拡張を使おう!
まさと・あかね:先生、ありがとう〜〜!
先生:また何かあったら、いつでも聞いてね!