[Tips] SwiftUIから楽にUIViewControllerを表示する
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