【Swift】ViewControllerからSwiftUIのViewを表示する

2022.08.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

SwiftUIからUIViewControllerUIViewを表示する際は、UIViewControllerRepresentableUIViewRepresentableを使用することで表示が出来ますが、UIViewControllerからSwiftUIを表示する方法を知りたかったので調べました。

環境

  • Xcode 13.3

UIHositngViewControllerを使用する

SwiftUIのViewUIViewControllerとして使用する為には、UIHostingViewControllerを使用します。

UIHostingViewControllerはSwiftUIのView階層を管理するUIViewControllerです。

SwiftUI ViewをUIKit View階層に統合する場合、UIHostingControllerオブジェクトを作成します。 作成時に、このViewControllerのroot viewとして使用するSwiftUI Viewを指定します。 rootViewのプロパティを使用して、後でそのビューを変更できます。HostingControllerを他のViewControllerと同じように使用するには、インターフェイスにchild ViewControllerとして表示するか、埋め込みます。

使用例

SwiftUIView

例として、下記のSwiftUIのViewを作成しました。このViewUIViewControllerから表示します。

import SwiftUI

struct SwiftUIView: View {

    @Environment(\.dismiss) private var dismiss

    var body: some View {

        ZStack {
            Rectangle()
                .fill(.yellow)
                .ignoresSafeArea()

            VStack(spacing: 16) {
                Text("SwiftUI View")

                Button {
                    dismiss()
                } label: {
                    Text("画面を閉じる")
                }
            }
        }
    }
}

ViewControllerから表示する

UIHostingViewController(rootView:)SwiftUIViewを指定してインスタンス化したものをpresent(_:, animated:)メソッドを使用するだけのシンプルなものになります。

import UIKit
import SwiftUI

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func presentSwiftUIView() {
        let controller = UIHostingController(rootView: SwiftUIView())
        controller.modalPresentationStyle = .overFullScreen
        present(controller, animated: true)
    }
}

デモ

このように表示することが出来ました。

ViewControllerToSwiftUI

UIKitライフサイクルを使用する

上記までは、表示前にUIHositingController(rootView:)を使用して呼び出すケースでしたが、UIHositingControllerクラスを用意することでライフサイクルを使用することも出来ます。

ViewModelを用意する

今回はviewDidLoadでSwiftUIのViewの背景色の変更を試してみたいと思います。値の変更を反映する為に、@ObsrvableObjectSwiftUIViewModelを用意しました。

import SwiftUI
import Combine

class SwiftUIViewModel: ObservableObject {

    @Published var backgroundColor: Color = .yellow
    let subject = PassthroughSubject<Void, Never>()
}

背景色をパブリッシュする為のbackgroundColorと、ボタンの押下イベントを発行する為のsubjectを宣言しています。

SwiftUIView

viewModelの定義をしている為、それに伴い、Rectangle().fill()の箇所と、Buttonの箇所を変更しています。

import SwiftUI
import Combine

struct SwiftUIView: View {

    @ObservedObject var viewModel: SwiftUIViewModel

    var body: some View {

        ZStack {
            Rectangle()
                .fill(viewModel.backgroundColor)
                .ignoresSafeArea()

            VStack(spacing: 16) {
                Text("SwiftUI View")

                Button {
                    viewModel.subject.send()
                } label: {
                    Text("画面を閉じる")
                }
            }
        }
    }
}

SwiftUIViewHostingController

UIHostingControllerをジェネリック型にする際には、ContentとなるSwiftUIのViewの記述が必要です。記載していない場合は、Xcodeが教えてくれます。

import SwiftUI
import Combine

class SwiftUIViewHostingController: UIHostingController<SwiftUIView> {

    private let viewModel: SwiftUIViewModel
    private var anyCancellable: AnyCancellable?

    init(viewModel: SwiftUIViewModel) {
        self.viewModel = viewModel
        super.init(rootView: SwiftUIView(viewModel: viewModel))
    }

    required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.backgroundColor = .red

        anyCancellable = viewModel.subject.sink { [weak self] _ in
            self?.dismiss(animated: true)
        }
    }
}

プロパティ

private let viewModel: SwiftUIViewModel
private var anyCancellable: AnyCancellable?

viewModelの値を変更してSwiftUIViewの見た目を変更する為に、SwiftUIViewModelを定義しています。また、ボタンの押下イベントを受け取るので、anyCancellableも用意しています。

init

引数として渡されたviewModelを自身のviewModelSwiftUIViewviewModelに渡してUIHostingControllerを初期化しています。

init(viewModel: SwiftUIViewModel) {
    self.viewModel = viewModel
    super.init(rootView: SwiftUIView(viewModel: viewModel))
}
viewDidLoad

viewDidLoad時に、背景色を.redに変更してみました。そして、ボタンの押下された際には、dismiss(animated:)メソッドを呼び出して画面を破棄するようにしています。

override func viewDidLoad() {
    super.viewDidLoad()

    viewModel.backgroundColor = .red

    anyCancellable = viewModel.subject.sink { [weak self] _ in
        self?.dismiss(animated: true)
    }
}

ViewControllerから表示する

ViewControllerpresentSwiftUIView()を実行した際に上記で作成したSwiftUIViewHostingControllerを渡すように変更しました。

import UIKit
import SwiftUI

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func presentSwiftUIView() {
        let controller = SwiftUIViewHostingController(viewModel: SwiftUIViewModel())
        controller.modalPresentationStyle = .overFullScreen
        present(controller, animated: true)
    }
}

デモ

無事に背景色が赤色に変更され、ボタン押下で画面を閉じることが出来ました。

ViewControllerToSwiftUI-2

おわりに

0からSwiftUIにリプレイスをすることは大変ですが、このように一部をSwiftUIに変更することは簡単に出来ることが判明した為、少しずつSwiftUIに変更していくのも可能だなと感じました。

WWDC22: Use SwiftUI with UIKitのセッションで説明があるのですが、iOS 16からUIHostingConfigurationを使用して、カスタムUICollectionViewUITableViewセルをシームレスに構築出来るようになるみたいです。楽しみですね!

参考