カテゴリー
iOS Swift

UIViewをSwiftUIで使いたい

筆者はSwiftUIからiOS開発を始めました。ストーリーボードを使わずにコードだけでViewを記述出来るSwiftUIは気に入っていますが、現時点でSwiftUIにないUIViewを使いたい場合は「橋渡し」としてUIViewRepresentableを使うこと実現出来ます。UIViewRepresentable protocolを理解する上で少々とっつきにくかったので記事にしようと思いました。

下記コードは最も基本的なカスタムUIViewをSwiftUIで使ってみている例です。

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
            BasicUIViewRepresentable() // 下記structをここでSwiftUIのViewと同様に使用できる
        }
    }
}
struct BasicUIViewRepresentable: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView { 
    // この必須関数はinitのような役目、some UIViewをreturnする
        let view = UIView() // ベーシックなUIViewインスタンスを生成
        view.backgroundColor = .red // わかり易いように赤色にしてるだけ
        return view
    }
    func updateUIView(_ uiView: UIViewType, context: Context) { 
   // この必須関数はSwiftUI側からデータを送りUIView側をアップデートする場合に使用される
    }
}

UIViewRepresentable protocolの記述を見るとより理解が深まると思います。意訳してみました。

public protocol UIViewRepresentable : View where Self.Body == Never {

    /// typealias UIViewType = UITextField などのように提供される型を指定できる
    associatedtype UIViewType : UIView

    /// Viewオブジェクトを生成時に一度だけ呼ばれ、初期ステートを設定する必須メソッド。
    /// 初回以降のViewのアップデートは``UIViewRepresentable/updateUIView(_:context:)``
    /// が呼ばれる。
    /// - パラメータ context: システムの現在のステートを保持するcontext struct
    /// - 戻り値: 提供された情報により設定されたUIKit view
    func makeUIView(context: Self.Context) -> Self.UIViewType

    /// SwiftUIからの情報をもとにViewを更新するメソッド。
    /// appのステートが変化する度に、SwiftUIは該当の情報(@State変数など)を更新し
    /// contextに応じてUIKit Viewの該当箇所を更新することができる。
    /// - パラメータ:
    ///   - uiView: カスタムViewオブジェクト
    ///   - context: システムの現在のステートを保持するcontext struct
    func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)

    /// 対象のUIKit Viewとcoordinatorをクリーンアップするメソッド。
    /// 例としてobserverを取り除く、などに使用できる。
    /// - パラメータ:
    ///   - uiView: カスタムViewオブジェクト
    ///   - coordinator: UIViewからSwiftUIへデータを渡すカスタムコーディネータインスタンス。
    ///     もしカスタムコーディネータを用いない場合は、システムによりデフォルトインスタンス
    ///     が提供される。
    static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)

    /// A type to coordinate with the view.
    associatedtype Coordinator = Void

    /// UIViewからSwiftUIへデータを渡すコーディネータインスタンスを生成。
    /// カスタムCoordinatorインスタンスを用いて@Bindingプロパティを扱うなどをしたい場合
    /// [以下の例: Coordinator]
    ///
    /// このメソッドはSwiftUIにより``UIViewRepresentable/makeUIView(context:)``の前に
    /// 呼ばれる。
    func makeCoordinator() -> Self.Coordinator

    typealias Context = UIViewRepresentableContext<Self>
}

下記はシンプルなUITextFieldViewの実装例

struct UITextFieldViewRepresentable: UIViewRepresentable {
    
    @Binding var text: String
    
    func makeUIView(context: Context) -> UITextField {
        let textField = getTextField()
        textField.delegate = context.coordinator as? UITextFieldDelegate
        return textField
    }
    
    // from SwiftUI to UIKit
    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
    }
    
    private func getTextField() -> UITextField {
        let textField = UITextField(frame: .zero)
        let placeHolder = NSAttributedString(string: "Type here", attributes: [.foregroundColor: UIColor.red])
        textField.attributedPlaceholder = placeHolder
        return textField
    }
    
    // from UIKit to SwiftUI
    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text)
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        
        @Binding var text: String
        
        init(text: Binding<String>) {
            self._text = text
        }
        private func textFieldDidChangeSelection(_ textField: UITextField) {
            text = textField.text ?? ""
        }
    }
    
    typealias UIViewType = UITextField
}