🔀

[Tips] SwiftUIから楽にUIViewControllerを表示する

2024/03/17に公開

SwiftUIからUIViewControllerを使う際のちょっとしたテクニックを紹介します。
平凡なテクニックですが、記事になっているものを見かけなかったので、記事に残しておきます。

SwiftUIからUIViewControllerを呼ぶ一般的な例

SwiftUIからUIViewControllerを使いたい場合、UIViewControllerRepresentableを用います。

struct ViewControllerWrapper: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> ViewController {
        ViewController()
    }

    func updateUIViewController(_ uiViewController: ViewController, context: Context) {
        // 更新処理
    }
}

これ自体は至極真っ当な方法ですが、1つのUIViewControllerの型に対して、毎回新しくUIViewControllerRepresentableの型を宣言する必要があって少し手間です。
個人的には命名などにも微妙に困ります。

ジェネリクスを用いて汎用的にする

ジェネリクスを用いて、汎用的な型を一つ用意します。
以下はその実装例です。
コードの量は少し長いですが、UIViewControllerRepresentableの各functionに対してclosureを外から差し込んでいるだけです。
その際にUIViewControllerやCoordinatorの具体的な型を外から指定できているのがキモです、

public struct UIViewControllerRepresenter<ViewController: UIViewController, Coordinator>: UIViewControllerRepresentable {
    private let makeCoordinatorHandler: @MainActor () -> Coordinator
    private let makeUIViewControllerHandler: @MainActor (Context) -> ViewController
    private let updateUIViewControllerHandler: @MainActor (ViewController, Context) -> Void
    private let sizeThatFitsHandler: @MainActor (ProposedViewSize, ViewController, Context) -> CGSize?

    public init(
        makeCoordinator: @escaping @MainActor () -> Coordinator = { () },
        makeUIViewController: @escaping @MainActor (Context) -> ViewController,
        updateUIViewController: @escaping @MainActor (ViewController, Context) -> Void = { _, _ in },
        sizeThatFits: @escaping @MainActor (ProposedViewSize, ViewController, Context) -> CGSize? = { _, _, _ in nil }
    ) {
        self.makeCoordinatorHandler = makeCoordinator
        self.makeUIViewControllerHandler = makeUIViewController
        self.updateUIViewControllerHandler = updateUIViewController
        self.sizeThatFitsHandler = sizeThatFits
    }

    public func makeCoordinator() -> Coordinator {
        makeCoordinatorHandler()
    }

    public func makeUIViewController(context: Context) -> ViewController {
        makeUIViewControllerHandler(context)
    }

    public func updateUIViewController(_ viewController: ViewController, context: Context) {
        updateUIViewControllerHandler(viewController, context)
    }

    @available(iOS 16.0, tvOS 16.0, *)
    @MainActor
    public func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: ViewController, context: Context) -> CGSize? {
        sizeThatFitsHandler(proposal, uiViewController, context)
    }
}

使用例

このような型を1つ用意しておけば、UIViewControllerを表示したいときにこれを呼ぶだけで十分です。
新しい型を宣言する必要はありません。

struct FooView: View {
    var body: some View {
        UIViewControllerRepresenter { _ in
            FooViewController()
        }
    }
}

もしCoordinatorが必要だったり、update時に何か処理が必要なら、それもクロージャーで差し込めます。

struct BarView: View {
    var body: some View {
        UIViewControllerRepresenter {
            BarCoordinator()
        } makeUIViewController: { context in
            let viewController = BarViewController()
            viewController.delegate = context.coordinator
            return viewController
        } updateUIViewController: { _, _ in
            print("updated")
        }
    }
}

class BarCoordinator: BarDelegate {
    // 略
}

注意

あくまでこれは型宣言を省略するための方法で、完全な柔軟性はありません。
例えば、UIViewControllerRepresentableのdismantleUIViewControllerはstatic関数なので外から注入できないため、ここではカバーしていません。

おわりに

SwiftUIからUIViewControllerを使う際のテクニックを紹介しました。
完全な柔軟性はないものの、一度プロジェクトに追加しておくと、使い回せる場面が多く、非常に便利な方法です。
この記事がみなさんの開発の手助けになれば、と思います。

Discussion