SwiftUIとUIKit間での値の受け渡し

こんにちは、けんご(@N30nnnn)です。

SwiftUI内でUIKitを使うには, UIViewRepresentable または UIViewControllerRepresentable をかませることで呼び出せることができます。 それはSwiftUI tutorialや多くの参考記事がある一方で、その間で変数をやり取りするのに詰まったのでメモに残します。

結論

データの受け渡しは以下の構成で行います。

  • Coordinator + ObservedObject
  • Coordinator + Binding

UIViewControllerRepresentable (SwiftUI) 内で Coordinator を実装し、 UIViewController (UIKit) にデリゲートすることで変数を受け取ります。 また、 Binding または ObservedObjectUIViewControllerRepresentable で受け取り、 Coordinator 内でそれらに値を代入することで、親Viewに値を返すことを実現します。

同じことなのでこの記事では ObservedObject で残します。

また、"デリゲート"とはデザインパターンの一種で、この記事が分かりやすいです。

[Swift]"デリゲートデザインパターン"ってなに? https://qiita.com/nitaking/items/d441c5aa2aceaf4fc089

ケース

次のような protocol を持つ Controller があった時に、 result を親の UIViewControllerRepresentable に送りたいとします。

public protocol SampleDelegate: class {
    func success(_ controller: UIViewController, result: String)
    func fail(_ controller: UIViewController,  error: String)
    func cancel(_ controller: UIViewController)
}

public class SampleController: UIViewController {
    public weak var delegate: SampleDelegate?
    ...
}

参考実装

SwiftUI側

値をやり取りする ObservableObject の定義

final class ResultModel: ObservableObject {
    @Published var result = ""
}

UIViewControllerRepresentable の定義と Coordinator の実装

値の代入を行う Coordinatorprotocol に則って実装し、 makeUIViewController 内で生成した SampleViewControllerインスタンスに実装した Coordinator をデリゲートします。

struct SampleViewControllerRepresentable: UIViewControllerRepresentable {
    // 親 View から初期化されたObservableObjectを受け取る
    @ObservedObject var model: ResultModel 
    
    func makeCoordinator() -> Coordinator {
        // Coordinator を定義した場合、 makeCoordinator も実装する必要有り
        Coordinator(self)
    }
    
    class Coordinator: NSObject, SampleDelegate {
        var parent: SampleViewControllerRepresentable
        
        init(_ sampleViewControllerRepresentable: SampleViewControllerRepresentable){
            self.parent = sampleViewControllerRepresentable
        }
        
        func success(_ controller: UIViewController, scanDidComplete result: String){
            print("result:\(result)")
            // ObservedObject に変数を代入。ここで値を受け取る。
            self.parent.model.result = result 
        }

        func fail(_ controller: UIViewController, error: String) {
            print("error:\(error)")
        }

        func cancel(_ controller: UIViewController) {
            print("Sample did cancel")
        }
    }

    func makeUIViewController(context: Context) -> SampleViewController {
        // SampleViewControllerの実装は後述
        let ctrl = SampleViewController() 
        ctrl.delegate = context.coordinator
        return ctrl
    }

    func updateUIViewController(_ uiViewController: SampleViewController, context: Context) {
    }
}

UIKit側

Controllerのインスタンスを生成とデリゲート

デリゲートを受け取る UIViewController を実装し、ケースで示した protocol を持つ Controllerインスタンスにデリゲートを伝播させます。

class SampleViewController: UIViewController {
    public weak var delegate: SampleDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        let ctrl = SampleController()
        ctrl.delegate = delegate
        self.present(ctrl, animated: true, completion: nil)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

最終的には、デリゲートしたメソッドが呼び出されて、 ObservableObject に代入されて親Viewで参照できます。