UINavigationControllerにUIViewControllerRepresentable配下のVCを加える。
背景
UIKit製のアプリをSwiftUIに切り替えていく方針で開発を進めています。このアプリの とある画面で問題が生じました。
問題は下記のような構成の画面で発生しました。
ナビゲーションバー・セグメント・ページビューで構成された画面とページビューでリンクされた各画面(以下内部コンテンツ)という構成です。
従来の構成
ナビゲーションバーとセグメントで制御されたページビューのイメージ。
Demoでは「Tap me」からさらに遷移する。
問題としては、内部コンテンツは既存のUIViewControllerを呼びつつそのコンテンツの制御UIはSwiftUIで行う構成(理想の構成)に変更を行ったところ、内部コンテンツの画面遷移で仕様を満たすことが、ただUIHostingControllerとUIViewControllerRepresentableを使うだけではできませんでした(問題の構成)。
理想の構成
問題の構成
その仕様は「内部コンテンツ内で特定のコンテンツを選択した際に共通のナビゲーションのプッシュ遷移を行う」です。
この問題に対して以下の対応を行いました。
結論
UINavigationControllerにUIViewControllerRepresentable配下のUIViewControllerを加えます。
具体的には
- UIHostingControllerの生成時に親のUIViewControllerの子にする。
- 内部コンテンツの生成時に親のUIHostingControllerの子にする。
解決のアプローチ
検証
下記を実際に検証していきます。
- UIHostingControllerの生成時に親のUIViewControllerの子にする。
- 内部コンテンツの生成時に親のUIHostingControllerの子にする。
準備
検証環境は以下です。
・MacBook Pro (14インチ、2021) OS: macOS Monterey 12.4
・Xcode 13.4.1
・iOS15.5
1. UIHostingControllerの生成時に親のUIViewControllerの子にする。
ViewControllerからUIHostingControllerを使ってSegmentPickerPageViewを生成します。
let viewController: UIHostingController = UIHostingController(rootView: SegmentPickerPageView())
config.hostingController = viewController
self.view.addSubview(viewController.view)
この時のView Hierarchyは以下のようになります。
一見するとViewControllerの傘下にありそうですがHosting View Controllerが見当たらないため、Viewとして配下にあるだけとなります。
下記のコードを書き加えます。
let viewController: UIHostingController = UIHostingController(rootView: SegmentPickerPageView())
config.hostingController = viewController
self.addChild(viewController) // 追加
self.view.addSubview(viewController.view)
View Hierarchyを確認します。
Hosting View Controllerが階層に追加されていることがわかります。
この対応だけではSegmentPickerPageViewが生成するSwiftUIView内の内部コンテンツのViewControllerは配下ではないためプッシュ遷移はできません。
2. 内部コンテンツの生成時に親のUIHostingControllerの子にする。
PageView(内部コンテンツとSegmentPickerPageViewを繋ぐSwiftUIView)ではUIViewControllerRepresentableを使用し従来のUIKit製の画面を呼び出しています。
makeUIViewControllerで内部コンテンツのViewControllerを生成します。
func makeUIViewController(context: Context) -> ViewControllerType {
let viewController = factory()
return viewController
}
内部コンテンツのViewControllerでは「Tap me」というUIButtonから遷移します。遷移コードは下記になります。
if self.navigationController != nil {
self.navigationController?.pushViewController(viewController, animated: true)
} else {
present(viewController, animated: true)
}
navigationControllerがあればプッシュ遷移、なければモーダル遷移になります。
この時のView Hierarchyは以下のようになります。
navigationControllerがなかったためモーダルで遷移しています。
Configurationというクラスを生成しUIHostingControllerを渡すようにし、渡されたUIHostingControllerの子として内部コンテンツのViewControllerを追加します。
final class Configuration {
weak var hostingController: UIViewController?
}
~省略~
func makeUIViewController(context: Context) -> ViewControllerType {
let viewController = factory()
config.hostingController?.addChild(viewController) // 追加
return viewController
}
class ViewController: UIViewController {
private let config = Configuration() // 追加
override func viewDidLoad() {
super.viewDidLoad()
~省略~
let viewController: UIHostingController = UIHostingController(rootView: SegmentPickerPageView(config: config)) // 修正
config.hostingController = viewController
self.addChild(viewController)
self.view.addSubview(viewController.view)
~省略~
}
}
View Hierarchyを確認します。
プッシュ遷移に成功したため親の階層はありません。親の階層は「1. UIHostingControllerの生成時に親のUIViewControllerの子にする。」の改善後と同じため割愛します。
まとめ
UIKitと組み合わせるのが一見難しそうなSwiftUIですが、UIKitと関連した問題の時 そのアプローチはUIKitだけで作るときに生じた問題と同じ解決のアプローチを使うことができると思いました。
SwiftUI側でも工夫は必要ですが焦らず問題を細く分解し一つ一つ見ていく必要があると思いました。
今回の検証に用いたデモはこちらです。
参考
Controlling UIHostingController with SwiftUI View
Navigation Controllers
State and data flow
【SwiftUI】PageTabViewStyle(UIPageViewController)の使い方【Xcode12&iOS14】
SwiftUI - How to access UIHostingController from SwiftUI
Discussion