[SwiftUI]iOS14から用意されたMapを使って動的なMapを開発してみる

## 投稿の経緯
SwiftUIでMapを表示するには[こちら](https://qiita.com/chino_tweet/items/73e6df230ab77a2f5887)にもあるように`UIViewRepresentable`を使ってカスタムViewを作成しなければなりませんでした。

しかし、iOS14からMapというSwiftUI用のViewが標準で搭載され、簡易的に実装することができるようになったので、個人開発中のアプリで使ってみたので、投稿します。

## 環境
Swift version 5.4.2
Xcode version 12.5.1

## MapKitをインポート
SwiftUIファイルを作成し（今回はMapView）、MapKitをインポートします。

```swift
import MapKit
```

## 必要な変数の準備
今回MapKitを扱うにあたって使用するイニシャライザは`CLLocationCoordinate2D`と`MKCoordinateSpan`です。なおこちら2点を扱うにあたって`MKCoordinateRegion`という状態変数が必要です。

各イニシャライザを要約して説明すると...

#### CLLocationCoordinate2D
表示領域の中心位置を緯度経度で決める

#### MKCoordinateSpan
緯度・経度それぞれに対する表示領域をそれぞれの縮尺（単位は度）で指定します。1度で約111kmあるそうなので比較的小さい値を指定することが多いと思います。例えば、100m を縮尺で表すと 0.0009 くらいになります。

また、引数として緯度経度を保存する`Double型`の変数を用意します。
`緯度 latitude`
`経度 longitude`

👇以下ここまでのコードです👇

```swift
import SwiftUI
import MapKit

struct MapView: View {
    @State private var region = MKCoordinateRegion() // 座標領域
    var coordinate: CLLocationCoordinate2D? // 表示領域の中心位置
    var latitude: Double // 緯度
    var longitude: Double // 経度
    
    /////以下省略/////
}
```

## Mapを使ってマップを表示
続いてMapを使ってViewに反映させるので、まずはコードを見ていただきます。

```swift
Map(coordinateRegion: $region,
    interactionModes: .all,
    showsUserLocation: true,
    userTrackingMode: $userTrackingMode,
    annotationItems: [
    PinItem(coordinate: .init(latitude: latitude, longitude: longitude))
    ],
    annotationContent: { item in
        MapMarker(coordinate: item.coordinate)
    })
```

少し複雑に見えますが、難しくはないのでひとつひとつ解説します。

#### coordinateRegion
`MKCoordinateRegion`の状態変数をバインディング指定します。(@State or @Published)

#### interactionModes
ユーザー操作の許可を以下の3つから設定します。


`.pan` スワイプ（ドラッグ）による操作を許可。
`.zoom` ダブルタッチ or ピンチ操作による拡大・縮小の操作を許可。
`.all` .pan と .zoom の両方を許可。

#### showsUserLocation
`true`でユーザーの現在位置を表示。

#### userTrackingMode
マップがユーザーの現在位置を追跡させるどうかを状態変数で管理します。

`.follow`　ユーザーを追跡します。
`.none`　ユーザーの追跡を停止します。

以下のように状態変数を準備して、指定してあげれば良いです。

```swift
@State private var userTrackingMode: MapUserTrackingMode = .none
```

#### annotationItems
マップ上で表示するピンの位置を緯度経度で指定します。ピンは`Identifiable`に準拠した構造体で定義し、`CLLocationCoordinate2D型`の緯度経度を管理させます。

```swift
struct PinItem: Identifiable {
    let id = UUID()
    let coordinate: CLLocationCoordinate2D
}
```

#### annotationContent
annotationItemsのピンのデザインを決めます。`MapMarker`、`MapPin`の二択です。

👇以下ここまでのコードです👇

```swift
import SwiftUI
import MapKit

struct PinItem: Identifiable {
    let id = UUID()
    let coordinate: CLLocationCoordinate2D
}

struct MapView: View {
    @State private var region = MKCoordinateRegion() // 座標領域
    @State private var userTrackingMode: MapUserTrackingMode = .none
    var coordinate: CLLocationCoordinate2D? // 表示領域の中心位置
    var latitude: Double // 緯度
    var longitude: Double // 経度
    
    var body: some View {
        Map(coordinateRegion: $region,
            interactionModes: .all,
            showsUserLocation: true,
            userTrackingMode: $userTrackingMode,
            annotationItems: [
                PinItem(coordinate: .init(latitude: latitude, longitude: longitude))
            ],
            annotationContent: { item in
                MapMarker(coordinate: item.coordinate)
            })
    }
}
```

## 引数で取得した緯度経度を使って表示領域の中心位置と縮尺を決める

```swift
private func setRegion(coordinate: CLLocationCoordinate2D) {
    region = MKCoordinateRegion(center: coordinate,
                                span: MKCoordinateSpan(latitudeDelta: 0.0009, longitudeDelta: 0.0009)
    )
}
```
プライベートな関数を用意して引数に`CLLocationCoordinate2D`を用意し、受け取った値は、`状態変数region`の`center`に指定。（ここで表示領域の中心位置が決まる）
次に、`span`に`MKCoordinateSpan`を指定し縮尺度合いを決定する。

そして、先ほど記述した`Map`に`.onAppear`を指定し、MapViewが呼ばれたら、まず`setRegion`が呼ばれるように調整する。

**※`.onAppear`はswiftでいうviewDidAppearのような扱いです。**

👇以下ここまでのコードです👇

```swift
import SwiftUI
import MapKit

struct PinItem: Identifiable {
    let id = UUID()
    let coordinate: CLLocationCoordinate2D
}

struct MapView: View {
    @State private var region = MKCoordinateRegion() // 座標領域
    @State private var userTrackingMode: MapUserTrackingMode = .none
    var coordinate: CLLocationCoordinate2D? // 表示領域の中心位置
    var latitude: Double // 緯度
    var longitude: Double // 経度
    
    var body: some View {
        Map(coordinateRegion: $region,
            interactionModes: .all,
            showsUserLocation: true,
            userTrackingMode: $userTrackingMode,
            annotationItems: [
                PinItem(coordinate: .init(latitude: latitude, longitude: longitude))
            ],
            annotationContent: { item in
                MapMarker(coordinate: item.coordinate)
            })
            .onAppear {
                setRegion(coordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude))
            }
    }
    
    // 引数で取得した緯度経度を使って動的に表示領域の中心位置と、縮尺を決める
    private func setRegion(coordinate: CLLocationCoordinate2D) {
        region = MKCoordinateRegion(center: coordinate,
                                    span: MKCoordinateSpan(latitudeDelta: 0.0009, longitudeDelta: 0.0009)
        )
    }
}
```

## 別ファイルからMapViewを呼び出す
```swift
 MapView(latitude: <Double>, longitude: <Double>)
```
となるので、`latitudeに緯度`を、`longitudeに経度`を指定してあげれば、MapViewの`.onAppear`が呼ばれ、`setRegion`の中で表示領域の中心位置と、縮尺が決定され、Viewが更新されます。

## お知らせ
現在、iOS開発案件を業務委託で募集中です（副業）。[TwitterDM](https://twitter.com/ken_sasaki2)でご依頼をお待ちしています。