UIKitビューをSwiftUI上で扱う

# この記事は何？
[SwiftUI Tutorials Creating and Combining Views](https://developer.apple.com/tutorials/swiftui/creating-and-combining-views)をハンズオンしたので、忘備録として要所と思われる部分を残しておきます。
ここでは、「UIKitビューをSwiftUI上で扱う方法」を取り上げます。

## 実行環境
macOS 10.15 Catalina beta7
Xcode11 beta7

# Use UIKit and SwiftUI Views Together
チュートリアルのセクション5の部分です。
このセクションを完了すると、以下のような画面ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/2829b537-fd68-e7a4-9eae-46068927ec19.png)

これを、単体の__カスタムSwiftUIビュー__として扱うことができます。

#### 説明文によると...
_MapKitのMKMapViewクラスを使用して、地図を描画します。
SwiftUI内でUIViewのサブクラスを使用するには、UIViewRepresentableプロトコルに準拠するSwiftUIビューで他のビューをラップします。_
...とあります。

`UIViewRepresentable` プロトコルに準拠させることが要点のようです。準拠したビューは「UIViewっぽく表示できる」ことが保証される感じでしょうか。
また、「SwiftUIビューで他のビューをラップする」とありますが、具体的にはどんな記述か調べます。

## ソースコード全体
完成したソースコードは、以下の通りです。
なお、`MapView_Previews` は単なるプレビュー画面を描画するための構造体なので、以降は無視します。

````Swift:SwiftUIのMapView
import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView {
        return MKMapView(frame: .zero)
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        let latitude = 34.011286, longitude = -116.166868
        let coordinate = CLLocationCoordinate2DMake(latitude, longitude)
        let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        uiView.setRegion(region, animated: true)
    }
    
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}
````

### フレームワークのインポート
当たり前な話ですが、地図を扱うので `MapKit` フレームワークを `SwiftUI` と合わせてインポートしています。
`UIKit`ビュー 自体を扱わなければ、`UIKit` をインポートする必要はないようです。

````Swift:
import SwiftUI
import MapKit
````

### MapView構造体
以下の `MapView` 構造体が、画面に表示されている地図そのものです。
型の名称が `MapView` となっていますが、カスタムSwiftUIビューなので実際には自由に命名できます。（例えば、`SwMapView` とか）

````Swift:MapViewの定義
struct MapView: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView {
        return MKMapView(frame: .zero)
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        let latitude = 34.011286, longitude = -116.166868
        let coordinate = CLLocationCoordinate2DMake(latitude, longitude)
        let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        uiView.setRegion(region, animated: true)
    }
    
}
````

コード量が削減できることは、`SwiftUI` で開発する利点の１つですが本当に少ないです！
実装は、2つのメソッドが定義されているだけです。

- makeUIView(context:) メソッド
- updateUIView(_:context:) メソッド

これらはいずれも、`UIViewRepresentable` プロトコルの準拠に必要なデリゲートメソッドです。
次のように、宣言されています。

````Swift:UIViewRepresentableプロトコル
/// 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)
````

それぞれのメソッドを見ていきます。

#### makeUIView(context:) メソッド
宣言によると `Self.Context` 型のパラメータを受け取り、`Self.UIViewType` 型オブジェクトを返します。

````Swift:メソッドの宣言
func makeUIView(context: Self.Context) -> Self.UIViewType
````
また、クイックヘルプのサマリーによると...
_表示する `UIView` インスタンスを生成する_
と記載されています。

````Swift:makeUIView()メソッドの呼び出し
func makeUIView(context: Context) -> MKMapView {    
    return MKMapView(frame: .zero)    
}
````

ここでは、`MKMapView(frame: .zero)` を返しているので、画面いっぱいに地図を表示できます。
実際には `MKMapView()` でも同じく画面全体に地図が表示されます。

#### updateUIView(_:context:) メソッド
宣言によると、パラメータは2つあります。

- `Self.UIViewType ` 型（実際には `MKMapView`）
- `Self.Context` 型（実際には `Context`）

なお、返り値はありません。

````Swift:メソッドの宣言
func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)
````
クイックヘルプのサマリーによると...
_表示された `UIView`（およびコーディネーター）を最新の構成にアップデートする_
と記載されています。

ここから読み取れる機能は「パラメータとしてビューの方を渡すと、コンテキストに合わせて描画を更新してくれる」と理解できます。
呼び出しコードを見ると、`MKMapView` 型と `Context` という型を渡しています。

````Swift:updateUIView()メソッドの呼び出し    
func updateUIView(_ uiView: MKMapView, context: Context) {
    let latitude = 34.011286, longitude = -116.166868
    let coordinate = CLLocationCoordinate2DMake(latitude, longitude)
    let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
    let region = MKCoordinateRegion(center: coordinate, span: span)
    uiView.setRegion(region, animated: true)
}
````

### Context
2つのメソッドにパラメータとして指定されている `Context` が気になります。
一般的には「文脈」とか「前後関係」という意味ですが、「いまの状況」と考えるといいかもしれません。
次のように宣言されていました。

````Swift:UIViewRepresentableの宣言
public protocol UIViewRepresentable : View where Self.Body == Never {
    ...
    typealias Context = UIViewRepresentableContext<Self>
}
````

`UIViewRepresentableContext<Self>` の別名です。
これ以上深入りするとハマりそうな予感がするので、「`UIViewRepresentable`に準拠すると使える値」と理解しておきます。

## UIViewRepresentableプロトコル
UIViewRepresentableプロトコル自体の宣言を見てみます。

````Swift:UIViewRepresentableプロトコルの宣言
public protocol UIViewRepresentable : View where Self.Body == Never {
    ...
}
````
`: View where Self.Body == Never` とある通り、前出のカスタムSwiftUIビュー `MapView` には、`body` がありません。
一般的なSwiftUIビューは以下のように、必ず `body` プロパティがあります。
`body` は計算プロパティになっており、ブロック内では宣言的にコードが記述されます。

````Swift:シンプルなカスタムSwiftUIビューの定義
struct CustomSwiftUIView: View {
    var body: some View {
        Text("Hello World!")
            .font(.title)
            .foregroundColor(.blue)
    }
}
````

比較してみると、実行文としてコードを記述する `update(_:context)` メソッドの実装は、`SwiftUI` 以前のスタイルで懐かしく見えます。

````Swift:updateUIView()メソッドの呼び出し    
func updateUIView(_ uiView: MKMapView, context: Context) {
    let latitude = 34.011286, longitude = -116.166868
    let coordinate = CLLocationCoordinate2DMake(latitude, longitude)
    let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
    let region = MKCoordinateRegion(center: coordinate, span: span)
    uiView.setRegion(region, animated: true)
}
````

この辺りのことを「SwiftUIビューで、UIビューをラップする」と表現していると思われます。