【Swift/UIKit】UIHostingControllerの使い方!Swift UIのViewを埋め込む

【Swift/UIKit】UIHostingControllerの使い方!Swift UIのViewを埋め込む

この記事からわかること

  • Xcode/iOSUIHostingController使い方する
  • Swift UIViewUIKitViewController埋め込むには?
  • UIHostingConfigurationとの違い

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Swift UIのViewをUIKitのViewControllerの中で使用する方法

Swift UIで構築したViewをUIKitのViewController管理しているUI構造の中に埋め込むためには2つの方法があります。

UIHostingControllerクラス

UIHostingControllerはSwift UIのViewを主に画面単位で追加したい場合に使用されます。UIViewControllerへ変換することができるので、そのままSwift UIのViewを新規画面にしたり、パーツとして組み込んだりすることが可能です。

UIHostingConfigurationクラス

UIHostingConfigurationはSwift UIのViewを主にリストのセルなどで活用したい場合に使用されます。UIContentConfigurationへ変換することができるので、そUICollectionView / UITableViewと簡単に統合することが可能です。

UIHostingControllerの使い方

公式リファレンス:UIHostingController

Swift UIで構築したViewをUIKitViewControllerで使用するためにはUIHostingControllerクラスを使用します。使い方は簡単でUIHostingController(rootView:)でSwift UIのViewをラップしてUIHostingControllerに変換します。

// Swift UIのViewを継承した構造体
let swiftUIView = MySwiftUIView()
// UIHostingControllerクラスに変換する
let hosting: UIHostingController = UIHostingController(rootView: swiftUIView)

UIHostingControllerContent(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)
    }
}

UIHostingControllerUIViewControllerなのでライフサイクルを保持しています。そのため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
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index