はじめに
UIHostingControllerでナビゲーションを非表示になるように実装していたのに、iOS 16のみナビゲーションが非表示にならず困ったため、その解決方法について記載します。
環境
- Xcode 13.4.1
- Simulator: iPhone 13 mini (iOS 15.5)
- Xcode 14.1
- Simulator: iPhone 14 Pro (iOS 16.1)
- 実機: iPhone 14 Pro (iOS 16.1.1)
現象
UIKitとSwiftUIでナビゲーションを非表示にしたViewControllerを作り、それぞれUITabBarControllerに配置します。
(後述のためにSwiftUI製のViewには中央にボタンを配置しておきます)
ナビゲーションを非表示にするために、以下の2つの処理を実行しています。
- SwiftUI側で
.navigationBarHidden(true)
をセット - UIKit側の
viewWillAppear
でnavigationController?.setNavigationBarHidden(true, animated: true)
を実行
Code
final class SwiftUIViewController: UIHostingController<SwiftUIView> {
private let viewModel = SwiftUIViewModel()
private var cancellable: Set<AnyCancellable> = []
init() {
super.init(rootView: SwiftUIView(viewModel: viewModel))
// viewModelのハンドリング
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = "SwiftUIView w/o NavigationBar"
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: true)
}
}
struct SwiftUIView: View {
@ObservedObject var viewModel: SwiftUIViewModel
var body: some View {
ZStack {
Color(.orange)
.frame(maxWidth: .infinity, maxHeight: .infinity)
Button(
"Go next (with NavigationBar)",
action: { viewModel.onTapButton.send() }
)
}
.ignoresSafeArea(edges: .top)
.navigationBarHidden(true)
}
}
Xcode 13.4.1でiOS 15.5のSimulatorにビルドして確認すると、以下のような画面が表示されます。
ところが、Xcode 13.4.1のDeviceSupportにiOS 16.1を追加した状態でiOS 16.1の実機にビルドすると、SwiftUIで実装した方の画面にナビゲーションが表示されてしまい、思ったような動作になっていませんでした。
Buttonを配置してナビゲーションを表示する画面と表示しない画面にpushするような実装をしてみても、同様にiOS 16.1.1ではナビゲーションが非表示になりません。ただし、一度別の画面に遷移したあとpopしてきたときにはナビゲーションが非表示になっていました。
iOS 15.5 (Simulator) | iOS 16.1.1 (実機) |
---|---|
![]() |
![]() |
解決方法
1. Xcode 14.x でビルドする
同じコードのまま、今度はXcode 14.1でビルドしてみます。
すると、Simulator、実機ともにiOS 16以上でもナビゲーションが表示されないようになりました。
iOS 16.1 (Simulator) | iOS 16.1.1 (実機) |
---|---|
![]() |
![]() |
2. UIHostingControllerのviewをUIViewController上に配置する
別の事情でXcodeのバージョンを上げることができない場合、UIHostingControllerのviewを別のUIViewControllerに配置することで解決できます。
本来表示したいUIHostingController SwiftUIViewController
の他にUIViewController SwiftUIParentViewController
を用意し、
-
SwiftUIViewController
のviewをSwiftUIParentViewController
のviewにaddSubviewする - ViewModelのハンドリングを
SwiftUIParentViewController
に移植する -
SwiftUIParentViewController
のviewWillAppear
でナビゲーションを非表示にする -
SwiftUIViewController
を呼び出していた箇所をSwiftUIParentViewController
を呼び出すように変更する
の4つの対応で簡単に対処することができます。
final class SwiftUIParentViewController: UIViewController {
private let viewModel = SwiftUIViewModel()
init() {
super.init(nibName: nil, bundle: nil)
// viewModelのハンドリング
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
super.loadView()
title = "SwiftUIView w/o NavigationBar"
// SwiftUIViewControllerの `view` を view に addSubview する
let vc = SwiftUIViewController(viewModel: viewModel)
guard let childView = vc.view else { return }
view.addSubview(childView)
childView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
childView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
childView.topAnchor.constraint(equalTo: view.topAnchor),
childView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
childView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// SwiftUIParentViewController 側でナビゲーションを非表示にする
navigationController?.setNavigationBarHidden(true, animated: true)
}
}
final class SwiftUIViewController: UIHostingController<SwiftUIView> {
init(viewModel: SwiftUIViewModel) {
super.init(rootView: SwiftUIView(viewModel: viewModel))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
iOS 15.5 (Simulator) | iOS 16.1.1 (実機) |
---|---|
![]() |
![]() |
3. UINavigationBar上の戻るボタンだけ非表示にする
上記の例では背景を全面的にオレンジ色にしていてナビゲーションが目立ってしまうため、何がなんでもナビゲーションを非表示にしたUIを実装しようとしていました。
ただ、左上の戻るボタンを非表示にしたいけどナビゲーションは表示でも非表示でもどちらでも構わない、というケースもあるかと思います。
そのような時は、ナビゲーションを非表示にする処理を削除し、SwiftUI側で .navigationBarBackButtonHidden(true)
をセットして戻るボタンだけ非表示にするというのも解決策の1つです。
final class SwiftUIViewController: UIHostingController<SwiftUIView> {
private let viewModel = SwiftUIViewModel()
init() {
super.init(rootView: SwiftUIView(viewModel: viewModel))
// viewModelのハンドリング
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = "SwiftUIView w/o NavigationBar"
}
}
struct SwiftUIView: View {
@ObservedObject var viewModel: SwiftUIViewModel
var body: some View {
ZStack {
Color(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
Button(
"Go next (with NavigationBar)",
action: { viewModel.onTapButton.send() }
)
}
.ignoresSafeArea(edges: .top)
.navigationBarBackButtonHidden(true) // 戻るボタンだけ非表示にする
}
}
iOS 15.5 (Simulator) | iOS 16.1.1 (実機) |
---|---|
![]() |
![]() |
![]() |
![]() |
iOS 15ではナビゲーションにタイトルと戻るボタンが一瞬表示されて非表示に、iOS 16ではタイトルは非表示にならず戻るボタンのみ一瞬表示されて非表示になる、という差分がありました。
この解決方法を選択する場合はタイトルをセットしないようにした方が良さそうです。
おわりに
新しいOSが公開されるのをいつもとても楽しみにしていますが、その反面アプリ開発者としては謎の不具合との出会いに悪戦苦闘したりします。
よりよい解決方法を模索しながら楽しく開発していきたいですね