【Swift/UIKit】UIHostingControllerの使い方!Swift UIのViewを埋め込む
この記事からわかること
- Xcode/iOSでUIHostingControllerの使い方する
- Swift UIのViewをUIKitのViewControllerに埋め込むには?
- UIHostingConfigurationとの違い
index
[open]
\ アプリをリリースしました /
環境
- Xcode:26.0.1
- iOS:26
- Swift:6
- macOS:Tahoe 26.0.1
Swift UIのViewをUIKitのViewControllerの中で使用する方法
Swift UIで構築したViewをUIKitのViewControllerで管理しているUI構造の中に埋め込むためには2つの方法があります。
- UIHostingControllerクラス・・・UIViewControllerへ変換する
- UIHostingConfigurationクラス・・・UIContentConfigurationへ変換する
UIHostingControllerクラス
UIHostingControllerはSwift UIのViewを主に画面単位で追加したい場合に使用されます。UIViewControllerへ変換することができるので、そのままSwift UIのViewを新規画面にしたり、パーツとして組み込んだりすることが可能です。
UIHostingConfigurationクラス
UIHostingConfigurationはSwift UIのViewを主にリストのセルなどで活用したい場合に使用されます。UIContentConfigurationへ変換することができるので、そUICollectionView / UITableViewと簡単に統合することが可能です。
UIHostingControllerの使い方
Swift UIで構築したViewをUIKitのViewControllerで使用するためにはUIHostingControllerクラスを使用します。使い方は簡単でUIHostingController(rootView:)でSwift UIのViewをラップしてUIHostingControllerに変換します。
// Swift UIのViewを継承した構造体
let swiftUIView = MySwiftUIView()
// UIHostingControllerクラスに変換する
let hosting: UIHostingController = UIHostingController(rootView: swiftUIView)
UIHostingControllerはContent(Swift UIのView)をラップしたUIViewControllerを継承しているクラスです。UIViewControllerを継承しているためviewDidLoadなどのライフサイクルも保持しており、UIKitベースの構造に違和感なく組み込めるようになっています。
@MainActor
@preconcurrency
open class UIHostingController<Content> : UIViewController where Content : View
viewプロパティ
viewプロパティにはSwift UIのViewがUIViewに変換されて紐づけられています。このviewプロパティを使用することでSwift UIであることを意識せずにUIKit側で扱うことが可能になります。
例えばUIKitで構築している画面にパーツとしてSwift UIのViewを差し込みたい場合は以下のように実装します。
class ViewController: UIViewController {
private let hosting = UIHostingController(rootView: MySwiftUIView())
override func viewDidLoad() {
super.viewDidLoad()
// ① 親VCに子VCを登録
addChild(hosting)
// ② 子のビューを親のビュー階層に追加
view.addSubview(hosting.view)
// ③ 子ビューのレイアウトを設定(frame / AutoLayout)
hosting.view.frame = CGRect(x: 0, y: 100, width: 300, height: 300)
// ④ 子ビューコントローラに通知
hosting.didMove(toParent: self)
}
}
UIHostingControllerはUIViewControllerなのでライフサイクルを保持しています。そのためaddSubviewだけで追加するのではなくaddChildで親のVCに追加し、didMoveで子ビューコントローラが親に追加されたことを通知しておきます。これをしないと正しくライフサイクルが機能しないことがあるので注意してください。
逆に取り除きたい場合はwillMoveを使用します。
@IBAction func remove() {
hosting.willMove(toParent: nil)
hosting.view.removeFromSuperview()
hosting.removeFromParent()
}
rootViewプロパティ
rootViewプロパティにSwift UIのViewが紐づけられています。データ型はint(rootView:)で指定したSwift UIのView構造体の型になっており明示的に更新することでUIも自動で再描画されます。ただこの方法での再描画では状態などもリセットされるので注意してください。
class ViewController: UIViewController {
private let hosting = UIHostingController(rootView: MySwiftUIView(title: "初期表示"))
override func viewDidLoad() {
super.viewDidLoad()
// hostingを画面に追加する処理
}
@IBAction func changeTitle() {
// rootViewを更新するとUIも自動で再描画される
hosting.rootView = MySwiftUIView(title: "Hello")
}
}
モーダル表示する
モーダル表示させたい場合はmodalPresentationStyleにモーダル表示させたい形式を指定しpresentメソッドを呼び出します。
@IBAction func presentModal() {
let modalHosting: UIHostingController = UIHostingController(rootView: MySwiftUIView(title: "モーダル"))
modalHosting.modalPresentationStyle = .automatic // => pageSheet / fullScreen
present(modalHosting, animated: true)
}
特に従来のモーダル表示と変わらないので詳細は以下の記事を参考にしてください。
データを渡す
Swift UIのViewに引数を設けておくことでデータを渡すことも可能です。
struct MySwiftUIView: View {
public let title: String
public let action: () -> Void
var body: some View {
VStack {
Text(title)
.font(.title)
Button {
action()
} label: {
Text("Tappd")
}
}.padding()
}
}
あとは呼び出す際にそれぞれ指定してあげます。
let swiftUiView = MySwiftUIView(title: "初期表示") { [weak self] in
guard let self else { return }
self.count += 1
}
データを同期させる
先ほどの方法だとデータを単一方向(UIKit=>Swift UI)に渡しているだけでした。実際のアプリではデータを双方向に連携させて扱うことが多いと思います。UIKitとSwift UI間でデータを同期して保持するためにはViewModelのような役割の中間クラス(今回はState)で管理します。
まずはObservableObjectを継承した中間クラスを定義します。
class AppState: ObservableObject {
@Published var counter: Int = 0
@Published var username: String = "Guest"
}
次にSwift UI側では@EnvironmentObject側で中間クラスを受け取って管理できるようにしておきます。
struct ContentView: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack(spacing: 20) {
Text("こんにちは、\(appState.username) さん!")
.font(.title)
Text("カウント: \(appState.counter)")
.font(.headline)
Button("SwiftUI側で増やす") {
appState.counter += 1
}
}
.padding()
}
}
最後にUIKit側ではenvironmentObjectを使用して中間クラスをSwift UIへ渡します。これでappStateインスタンスを通してデータが双方間で同期されるようになります。
class ViewController: UIViewController {
private let appState = AppState()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// Swift UIのViewを埋め込む
let contentView = ContentView().environmentObject(appState)
let hostingController = UIHostingController(rootView: contentView)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
hostingController.view.heightAnchor.constraint(equalToConstant: 200)
])
hostingController.didMove(toParent: self)
// UIKitのボタンを実装
let button = UIButton(type: .system)
button.setTitle("UIKit側で減らす", for: .normal)
button.addTarget(self, action: #selector(decrement), for: .touchUpInside)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.topAnchor.constraint(equalTo: hostingController.view.bottomAnchor, constant: 30)
])
appState.$counter
.sink { value in
print("UIKit 側でカウンター更新を検知: \(value)")
}
.store(in: &cancellables)
}
@objc func decrement() {
appState.counter -= 1
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。







