【SwiftUI】MapKitを使ってマップ検索アプリを作る

## はじめに
SwiftUIでマップが表示できる **『MapKit』** を使う方法を学習したので、内容をまとめます。

## 環境
【Xcode】16.2
【iOS】18.2
【macOS】Sequoia 15.3.1

## MapKitとは
Appleが提供するフレームワークの一つで、地図関連の機能をアプリに組み込むことができます。
地図を表示したり、ユーザーの位置情報を表示したり、場所をピンでマークしたりすることができます。

## MapKitを使ってマップ検索アプリを作成する流れ
1. 地図を表示する機能を持つMapView構造体を、MapView.swiftに記述する。
2. 検索キーワードを設定し、キーワードから緯度経度を検索できるようにする。
3. 緯度軽度の検索結果を使って、マップ上にピンを置く。
4. 検索画面を作成して、入力されたキーワードをもとに地図が表示されるようにする。

## 作成方法
### 1. 地図を表示する機能を持つMapView構造体を、MapView.swiftに記述する。
まずは、地図を表示する部品となるマップパーツを作ります。
このパーツは指定されたキーワードで検索してマップを表示する役割を持っており、1つのファイルにまとめて記述するようにします。

MapView構造体を作成するMapView.swiftファイルを新たに作成します。
**command + N** のショートカットキーで、新しいファイルを作成することができます。
テンプレートは **SwiftUI view** を選択します。
![スクリーンショット 2025-03-04 10.26.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4004493/389d977b-687d-49bb-9d07-0a64aba9d28c.png)

作成したMapView.swiftファイル内で、
``` swift
import MapKit
```
と記述することでMapKitフレームワークを読み込みます。

MapKitを読み込んだら、
``` swift
Map()
```
と記述するだけでマップを表示することができます。

### 2. 検索キーワードを設定し、キーワードから緯度経度を検索できるようにする。
続いて、検索キーワードを受け取るための仕組みを作ります。

![スクリーンショット 2025-03-04 10.53.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4004493/0e2a2850-a57e-48ee-b341-d2b06079f4e2.png)
``` swift
//検索キーワード
let searchKey: String
```
で検索キーワードを保存するための定数 searchKey を追加します。
構造体MapViewでは searchKey の値は変化しないので、定数として宣言します。

``` swift
Map() {

}
//検索キーワードの変更を検知
.onChange(of: searchKey, initial: true) { oldValue, newValue in
     print("検索キーワード：\(newValue)") 
}
```
Map() の後にクロージャを記述します。（後ほど、マップに表示したいピンを指定する処理をクロージャ内に記述します。）
**.onChange** は、ある値が変更されたときに処理を実行できるモディファイアです。
第一引数 of で指定されている searchKey の値が変更されたとき、 print("検索キーワード：\(newValue)") が実行されます。

第二引数の initial は、このViewが最初に表示されたときにアクションを実行するかを指定できます。
trueとしているためアクションを実行します。

クロージャ内の第一引数 oldValue は、valueが変更される前の値がセットされています。
第二引数 newValue には変更後の値がセットされています。
プレビューで searchKey に"東京駅"がセットされているので、 newValue にも"東京駅"がセットされ、デバッグエリアにも出力されます。

![スクリーンショット 2025-03-04 11.38.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4004493/2092ac38-863d-4c7f-a2ef-4e647a84c3ab.png)

``` swift
//キーワードから取得した緯度経度
@State var targetCoordinate = CLLocationCoordinate2D()
```
で、検索キーワードから取得した位置情報を格納する変数を宣言します。
**CLLocationCoordinate2D** は緯度経度の情報を格納できるデータ型です。
CLLocationCoordinate2D() で位置情報オブジェクトを作成しています。

``` swift
//地図の検索クエリ（命令）の作成
let request = MKLocalSearch.Request()
```

**MKLocalSearch** はマップベースの検索を開始し、結果を処理するためのオブジェクトです。
**Request()** オブジェクトは、文字列に基づいてマップの場所を検索する場合に作成します。
あとで入力されたテキストをこのオブジェクトに設定すると、検索処理が開始されます。

``` swift
//検索クエリにキーワードの設定
request.naturalLanguageQuery = newValue
```

**naturalLanguageQuery** プロパティに検索したい文字列をセットします。
onChange モディファイアにより newValue には検索するキーワードがセットされています。

``` swift
//MKLocalSearchの初期化
let search = MKLocalSearch(request: request)
```

検索を実行するためのオブジェクトを作成しています。
requestには検索のリクエスト情報が格納されています。

``` swift
//検索の開始
search.start { response, error in
    //結果が存在する時は1件目を取り出す
    if let mapItems = response?.mapItems,
    　　let mapItem = mapItems.first {

        //位置情報から緯度経度をtargetCoordinateに取り出す
        targetCoordinate = mapItem.placemark.coordinate
    }
}
```

**start** メソッドで検索を開始します。位置情報の取得が完了したときに、in以降の処理が実行されます。
キーワードからの検索では、一つの場所を特定できないときは複数の位置情報がAppleのサーバーから送られてくる時があります。**mapItems** も複数の検索結果が格納されている可能性があるため、配列で管理されています。
そのため、 配列の1列目が最も目的に近い情報であると仮定して、**mapItems.first** で先頭の配列を取り出します。

**placemark** には、その位置に関する住所、地名などの情報が格納されており、placemarketにcoordinateプロパティを指定することで緯度経度が取得できます。

### 3. 緯度軽度の検索結果を使って、マップ上にピンを置く。
続いて、取得した緯度経度情報をもとにピンをマップ上に置きます。
そして、ピンが画面の中央に来るようにマップを移動させます。
![スクリーンショット 2025-03-07 6.37.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4004493/d0187fc1-1b36-45b1-a000-de8e7ea26d58.png)

``` swift
//表示するマップの位置
@State var cameraPosition: MapCameraPosition = .automatic
```
**MapCameraPosition 構造体**は、マップ内のカメラの位置を記述する構造体です。
初期値は .automatic で、見え方をシステムに委ねています。
cameraPosition の変化によってマップを再描画させるため、@State で宣言します。

``` swift
Map(position: $cameraPosition) 
```
Mapの引数positionでカメラの位置を指定できます。
cameraPositionは、あとでキーワードの検索結果を利用してカメラの位置を設定します。
cameraPositionの値が変化するとMapが再描画されます。

``` swift
//マップ上にピンを表示
Marker(searchKey, coordinate: targetCoordinate)
```
を Map() のクロージャ内に記述することで、Mapが描画されるときにピンを配置します。
searchKeyには検索キーワード、coordinateには検索キーワードの緯度経度がセットされています。

![スクリーンショット 2025-03-07 6.57.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4004493/a52724b1-086e-43cf-9a52-cc7b8e87b9d9.png)

``` swift
//表示するマップの領域を作成
cameraPosition = .region(MKCoordinateRegion(
    center: targetCoordinate,
    latitudinalMeters: 500.0,
    longitudinalMeters: 500.0
```
取得した緯度経度情報を使って、画面に表示させるマップの位置とサイズを作成しています。
**.region** は、カメラの位置を作成するためのMapKitのメソッドです。
**MKCoordinateRegion** は、緯度と経度を中心とした長方形の地理的領域を示す構造体です。
centerにtargetCoordinateをセットしており、targetCoordinateには検索キーワードから取得した緯度経度が含まれているので、検索結果が中心になるようにマップを移動します。
latitudinalMeters,longitudinalMertersはマップの粒度を示しており、500.0をセットすると中心から500メートル範囲がViewに収まるように表示されます。

### 4. 検索画面を作成して、入力されたキーワードをもとに地図が表示されるようにする。
続いて、CotentView.swift内で検索画面を作成していきます。

![スクリーンショット 2025-03-07 7.20.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4004493/34f0452a-ee9d-4db9-9df2-8230be3987b9.png)

textFieldで入力中の文字列が保存される変数inputTextと、入力が完了した文字列を保存する変数displaySearchKeyを宣言します。
displaySearchKeyには初期値として東京駅がセットされているので、アプリ起動時には東京駅を中心にマップが表示されます。

``` swift
//入力が完了されたとき
    .onSubmit {
        //入力が完了したので検索キーワードに設定する
        displaySearchKey = inputText
    }
```
textFieldに **.onSubmit** モディファイアを付けることで、ユーザーが入力して改行を行ったタイミングで{}内のコードが実行されます。
ここでは、キーワードの入力が完了したときにinputTextからdisplaySearchKeyへ値がコピーされ、Viewが更新されます。

``` swift
//マップを表示
MapView(searchKey: displaySearchKey)
```
MapViewの引数searchKeyに、検索キーワードが格納されているdisplaySearchKeyを指定してマップを表示します。

## まとめ
・MapKitは、地図関連の機能をアプリに組み込むことができるフレームワーク。
・import MapKitでMapKitフレームワークを読み込み、地図を表示するMapView構造体を作成
・MKLocalSearchを利用し、入力したキーワードから位置情報を検索
・.onChangeモディファイアでキーワードの変更を検知し、検索を実行
・Markerを使ってピンをマップ上に表示
・MapCameraPositionでマップの表示位置を検索結果に合わせて更新
・TextFieldでユーザーからの入力を受け付け、検索キーワードを管理
・.onSubmitで入力完了時にマップを更新