UIKitのカスタムビュー側の値をSwiftUIのView側で取得する方法

## はじめに
SwiftUIだけでは用意されていないコンポーネントがあったりでUIKit使いたいなーという状況はよくあるかと思います。

そんな時は、SwiftUIで頑張らないで、UIKitで作ってしまえ...と思い、カスタムビューを作成したのですが、`カスタムビュー側で保持している値`を`SwiftUIのView側で使いたい`という状況に遭遇したので、  
今回はその方法を備忘録兼ねて残します。

また、今回は、SwiftUIの方にないSearchBarをUIKitで作り、`UISearchBar`のテキストフィールドに入力された値をSwiftUI側で取得するというサンプルで試してみます。

## 環境
Xcode11 GM SEED
SwiftUI
Mac OS 10.14.6（18G103）

## まずXibを使ったカスタムビューを作成しておく

以下のようにカスタムビューを`UIView`で定義しておきます。
今まで通りAutoLayoutはしっかり設定させます。

アウトレットでついているのは、`UISearchBar`とカスタムビュー自体のContentViewだけ。
そしてXib側ではLabelを追加しています。
また、見やすいようにUIKitで作成したカスタムビューは青色にしておきます。


```swift
import UIKit

// Define UIKitSearchView in the usual way to create custom views.

class UIKitSearchView: UIView {
    @IBOutlet weak var contentView: UIView!
    @IBOutlet weak var searchBar: UISearchBar!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    private func commonInit() {
        Bundle.main.loadNibNamed("UIKitSearchView", owner: self, options: nil)
        contentView.frame = bounds
        addSubview(contentView)
        
        searchBar.placeholder = "UIKitSearchView's searchBar"
        searchBar.backgroundImage = UIImage()
    }
}
```
<img width="906" alt="スクリーンショット 2019-10-01 20.51.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184792/023d0583-a37d-6851-3cd4-110128aa5fa2.png">

これでカスタムビューの準備は終わりました。

## UIViewRepresentableを準拠したSwiftUISearchViewを定義する
SwiftUIで宣言されたViewの中でUIViewを使う場合は、`UIViewRepresentable`プロトコルを使用したViewでラップすることが必要です。

この`SwiftUISearchView`がSwiftUIで使う為のラップビューとなります。

```swift
import SwiftUI

struct SwiftUISearchView: UIViewRepresentable {
    
    func makeUIView(context: Context) -> UIKitSearchView {
        print("\(#function)")
        return UIKitSearchView()
    }

    func updateUIView(_ uiView: UIKitSearchView, context: Context) {
        print("\(#function)")
    }
}
```

シンプルにUIViewを表示させるだけであれば、この`SwiftUISearchView`を宣言すれば終わりですが、このままでは`UISearchBar`に入力された文字列を取得することができません。

## UIViewRepresentableの定義を見てみる
UIKitSearchView側でクロージャを保持するプロパティを定義したり、強引にやればなんとかなりそうですが、スマートじゃないってことで
`UIViewRepresentable`のプロトコルの定義を調べてみます。

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

    /// The type of `UIView` to be presented.
    associatedtype UIViewType : UIView

    /// Creates a `UIView` instance to be presented.
    func makeUIView(context: Self.Context) -> Self.UIViewType

    /// Updates the presented `UIView` (and coordinator) to the latest
    /// configuration.
    func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)

    /// Cleans up the presented `UIView` (and coordinator) in
    /// anticipation of their removal.
    static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)

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

    /// Creates a `Coordinator` instance to coordinate with the
    /// `UIView`.
    ///
    /// `Coordinator` can be accessed via `Context`.
    func makeCoordinator() -> Self.Coordinator

    typealias Context = UIViewRepresentableContext<Self>
}

@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@available(OSX, unavailable)
extension UIViewRepresentable where Self.Coordinator == Void {

    /// Creates a `Coordinator` instance to coordinate with the
    /// `UIView`.
    ///
    /// `Coordinator` can be accessed via `Context`.
    public func makeCoordinator() -> Self.Coordinator
}

@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@available(OSX, unavailable)
extension UIViewRepresentable {

    /// Cleans up the presented `UIView` (and coordinator) in
    /// anticipation of their removal.
    public static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)

    /// Declares the content and behavior of this view.
    public var body: Never { get }
}
```

ふむふむ。
なんだかUIViewRepresentableを継承すると、以下の実装が必要となるようです。

- `makeUIView(context:)`
- `updateUIView(_:context:)`
- `makeCoordinator()`
- `dismantleUIView(_ uiView:coordinator:)`

ただ、`makeCoordinator()`、`dismantleUIView(_ uiView:coordinator:)`はextensionでデフォルト実装がされているので、定義不要みたいですね。

## なんかmakeCoordinator()が使えそう
`dismantleUIView`は調べたところ、表示されるUIKitビュー（およびコーディネーター）をクリーンアップするのに使うらしい。(よくわかりませんでした。)
どなたか教えていただけると嬉しいです。

ということで`makeCoordinator`がなんか使えそう。

ググってみると
なんでも、この`Coordinator`を使用するとデリゲートやデータソース、ユーザーの操作に対するイベントハンドリングのようなUIKitなどで実装していた一般的な処理をSwiftUIでも実装できるらしい。

```swift
extension UIViewRepresentable where Self.Coordinator == Void {

    /// Creates a `Coordinator` instance to coordinate with the
    /// `UIView`.
    ///
    /// `Coordinator` can be accessed via `Context`.
    public func makeCoordinator() -> Self.Coordinator
}
```
`UIViewRepresentable`を継承したSelfにある`Coordinator`がなければ、デフォルト実装をするようになっているみたいなので、
つまりは、さきほど作った`SwiftUISearchView`の中に`Coordinator`を作ればいいっぽい。  
そして、この`Coordinator`に`UISearchBarDelegate`をつけちゃいます。

`Coordinator`を作ったら`makeCoordinator`も追加して、さらに`delegate`も指定させます。


```swift
struct SwiftUISearchView: UIViewRepresentable {

    func makeCoordinator() -> SwiftUISearchView.Coordinator {
        return Coordinator()
    }

    func makeUIView(context: Context) -> UIKitSearchView {
        print("\(#function)")
        let view = UIKitSearchView()
        view.searchBar.delegate = context.coordinator
        return view
    }

    func updateUIView(_ uiView: UIKitSearchView, context: Context) {
        print("\(#function)")
    }
}
extension SwiftUISearchView {
    class Coordinator: NSObject, UISearchBarDelegate {
        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            print("\(#function)")
        }
        
        func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
            print("\(#function)")
        }
    }
}
```

おー、これでCoodinatorで通知を受け取れるようになりました。

## UISearchBarのテキストに入力した値を取得する
`@EnvironmentObject`を使えば簡単ですが、それだと設計上良くなさそう。

なので、今回は、`@State` + `@Binding`で実現させてみます。

### おさらい
### `@State`とは?
SwiftUIだとViewはstructなので、値の更新ができないけれども
@Stateを宣言すると、値を更新することができるようになるバインド用の修飾子。

### `@Binding`とは?
親Viewの@State attributeがついた変数などの値の更新通知を受け取れるようにするバインド修飾子のようです。(なんか使えそう...)
僕の中でこいつはまだふわっとしています。アドバイスお待ちしております

## バインドさせるイメージ

1.親のContentViewでtextを保持  
2.子のSwiftUISearchViewに親のContentViewで定義したtextを渡して同期させる  
3.UISearchBarの変更通知を受けるのはCoordinatorなので、SwiftUISearchViewからCoordinatorへもtextを渡して同期させる  

こんな感じで行けるんじゃないかと思っています。
それでは、残りをやっていきます。

## 親ビューを作ってtextをバインドさせる
まず親ViewであるContentView作ります。
そこで@StateでUISearchBarからの入力値を保持しておく受け皿を用意しておきます。



```swift
struct ContentView: View {
    // 親ビューで値が知りたいので、@Stateでtextを定義しておく
    @State private var text: String = DEFAULT_TEXT
    
    var body: some View {
        VStack(alignment: .center) {
            // バインドさせたいのでSwiftUISearchViewの初期化処理で突っ込む
            SwiftUISearchView(text: $text)
                .frame(height: 100.0)
            Spacer()
            VStack {
                Text("SwiftUI View")
                    .font(.title)
                    .padding(.bottom, 20)
                Text(text)
            }
            .multilineTextAlignment(.center)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color(.blue).opacity(0.5))
            .padding()
            Spacer()
        }
    }
}
```

## SwiftUISearchViewとCoordinatorにもtextを保持するようにプロパティと処理を追加
親ビューに続いて、SwiftUISearchView、Coordinatorにもtextをバインドさせる為に、@Bindingでプロパティを追加し、さらに初期化やデリゲート通知を受けた時の処理を追加しておきます。

```swift
struct SwiftUISearchView: UIViewRepresentable {
    @Binding var text: String
    
    func makeCoordinator() -> SwiftUISearchView.Coordinator {
        return Coordinator(text: $text)
    }
    
    func makeUIView(context: Context) -> UIKitSearchView {
        print("\(#function)")
        let view = UIKitSearchView()
        view.searchBar.delegate = context.coordinator
        return view
    }

    func updateUIView(_ uiView: UIKitSearchView, context: Context) {
        print("\(#function)")
    }
}

extension SwiftUISearchView {
    class Coordinator: NSObject, UISearchBarDelegate {
        @Binding var text: String
        
        init(text: Binding<String>) {
            _text = text
        }
        
        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            print("\(#function)")
            text = searchText.isEmpty ? DEFAULT_TEXT: searchText
        }
        
        func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
            print("\(#function)")
            text = DEFAULT_TEXT
            searchBar.text = nil
        }
    }
}
```
ここまでで一旦完成しました。
あとは実行して確認します。

## 結果
実行してみると無事値がSwiftUI側の親ビュー側に同期されていました。
イメージ通りです。
これは`@Binding`便利です。

<img width="300" alt="swiftui_sample_gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184792/59cf91d4-4bf4-1387-c86e-b5265ce91b61.gif">

## github
https://github.com/kazy-dev/UISearchbar-from-SwiftUI-Sample

## まとめ

今回は、`UIKitのカスタムビュー側の値`を`SwiftUIのView側で取得する`という方法をやってみましたが、なんとかイメージ通りに機能したので、すっきりしました。

ただ、これが正しいのかが正直良くわかっていませんので
アドバイスやご指摘などあれば、ぜひぜひコメントお願いいたします。

SwiftUI関連は、これから色々最適解が出てくると思いますので、それまではトライアンドエラーでやってみたいと思います。

それでは、最後までお読みいただきありがとうございました。