SwiftUIからUIKitの部品を使う


概要

SwiftUIからUIKitの部品を使うときの方法を紹介します。
サンプル用にシンプルな動作を考えました。
ボタンタップ時にラベルの文字列を変えるだけです。
動画にすると下のような感じです。

環境

Xcode 13.1
macOS Big Sur 11.6

手順

  1. UIViewRepresentableに準拠した構造体を作る
  2. UIKitから画面を作成するためにmakeUIViewを実装する
  3. 作成した画面が更新できるようにupdateUIViewを実装する
  4. UIKitからSwiftUIが保持している値を変更できるようにする

キーワードの解説

SwiftUIからUIKitを扱うにあたりキーワードがいくつかあります。
代表的なのはmakeUIView(context:), updateUIView(_:context:), Coordinatorです。
公式の説明を下に載せます。

makeUIView(context:)

ビューオブジェクトを作成し、その初期状態を構成します。

updateUIView(_:context:)

SwiftUIからの新しい情報で、指定されたビューの状態を更新します。

Coordinator

  1. システムは、ビュー内で発生した変更をSwiftUIインターフェースの他の部分に自動的に伝達しません。
  2. ビューを他のSwiftUIビューと調整する場合は、それらの相互作用を容易にするためにCoordinatorインスタンスを提供する必要があります。
  3. たとえば、コーディネーターを使用して、ターゲットアクションを転送し、ビューから任意のSwiftUIビューにメッセージを委任します。

実装

以下実装を進めます。

ボタンの初期状態を実装する

makeUIView(context:)UIButtonを返してあげます。

Views/CustomButton.swift
struct CustomButton: UIViewRepresentable {
    func makeUIView(context: Context) -> UIButton {
        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 160, height: 44))
        button.setTitle("Custom Button", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.addTarget(context.coordinator, action: #selector(Coordinator.didTapCustomButton(sender:)), for: .touchUpInside)
        return button
    }
}

ボタンのスタイルを更新するためのメソッドを追加する

中身は空ですがUIViewRepresentableプロトコルに準拠するために必須のメソッドです。

Views/CustomButton.swift
struct CustomButton: UIViewRepresentable {
...
    func updateUIView(_ uiView: UIButton, context: Context) {
    }
...
}

ボタンタップ時にラベルの文字列を変更できるようにCoordinatorクラスを実装する

名前解決にもつながるため、CustomButtonクラスの中でCoordinatorを宣言します。

Views/CustomButton.swift
struct CustomButton: UIViewRepresentable {
...
    func makeCoordinator() -> Coordinator {
        return Coordinator(button: self)
    }

    class Coordinator {
        var button: CustomButton

        init(button: CustomButton) {
            self.button = button
        }

        @objc func didTapCustomButton(sender: UIButton) {
            if button.text == "Default" {
                button.text = "Change"
                return
            }
            button.text = "Default"
        }
    }

...
}

最終的なコードは以下です。

Views/CustomView.swift
import SwiftUI

struct CustomButton: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UIButton {
        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 160, height: 44))
        button.setTitle("Custom Button", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.addTarget(context.coordinator, action: #selector(Coordinator.didTapCustomButton(sender:)), for: .touchUpInside)
        return button
    }

    func updateUIView(_ uiView: UIButton, context: Context) {
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(button: self)
    }

    class Coordinator {
        var button: CustomButton

        init(button: CustomButton) {
            self.button = button
        }

        @objc func didTapCustomButton(sender: UIButton) {
            if button.text == "Default" {
                button.text = "Change"
                return
            }
            button.text = "Default"
        }
    }
}

SwiftUIから呼び出す

UIKitの部品をSwiftUIから呼び出します。
記事では以下のようにしました。

Home.swift
import SwiftUI

struct Home: View {
    @State var text: String = "Default"

    var body: some View {
        VStack {
            Text(text)
            CustomButton(text: $text)
        }
    }
}

struct Home_Previews: PreviewProvider {
    static var previews: some View {
        Home()
    }
}

以上のコードで動画のような動作になると思います。