MapKitで地点情報のサジェスト表示

## はじめに
[MapKit](https://developer.apple.com/documentation/mapkit/)は、appleより提供されている地図や衛星写真を取り扱うためのフレームワークです。
UI上に地図を表示する以外にも様々なことが可能です。
今回は、入力中に候補となる地点情報を提案する「サジェスト機能」としての使い方を紹介します。

## 環境
- OS: macOS Sonoma 14.7.1
- XCode: 16.2.0

## サジェスト内容を取得する
### 構成
以下のような構成でアプリを作成することにしてみます。
なお、`ContentView`と`ContentViewModel`、`SuggestModel`に関しては今回の主題と外れるので、完成したコードのみ示し、主にSuggestRepositoryに付いて解説します。

#### ContentView
Viewの描画を行う。
入力欄と、サジェスト内容のリストを持つ
```Swift
struct ContentView: View {
    @State var viewModel = ContentViewModel()

    var body: some View {
        VStack {
            TextField("検索", text: $viewModel.searchText)
                .onChange(of: viewModel.searchText) {
                    Task  { await viewModel.requestSuggets() }
                }

            List(viewModel.suggests, id: \.self) { suggest in
                VStack(alignment: .leading) {
                    Text("名前: \(suggest.name)")
                    Text("緯度: \(suggest.latitude)")
                    Text("軽度: \(suggest.longitude)")
                }
            }
        }
        .padding()
    }
}
```

#### ContentViewModel
ContentViewのViewModel。
サジェスト内容の一覧`suggests`や検索窓の入力内容`searchText`を持つ
```Swift
@MainActor
@Observable
final class ContentViewModel {
    var suggests = [SuggestModel]()
    var searchText: String = ""

    /// SuggestRepositoryからサジェスト内容の一覧を受けとり、`suggests`を更新する
    func requestSuggets() async {
        let suggests = await SuggestRepository.shared.requestSuggests(query: searchText)
        self.suggests = suggests
    }
}
```

#### SuggestModel
サジェスト内容を保持する。
SuggestModelの配列をSugegstRepositoryから返すことでContentViewModelにサジェスト情報を伝える。
今回はサンプルとして場所の名前と緯度経度を返すことにする。
```Swift
struct SuggestModel: Hashable {
    var name: String
    var latitude: Double
    var longitude: Double
}

```

### SuggestRepositoryの実装
実装自体はシンプルなので、まずコードを示します。
```Swift
import MapKit

actor SuggestRepository {
    static let shared = SuggestRepository()

    private init() {}

    func requestSuggests(query: String) async -> [SuggestModel] {
        let localSearch = MKLocalSearch.Request()
        localSearch.naturalLanguageQuery = query

        let response = try? await MKLocalSearch(request: localSearch).start()

        guard let mapItems = response?.mapItems else { return [] }
        let suggests = mapItems.compactMap {
            SuggestModel(
                name: $0.name ?? "",
                latitude: $0.placemark.coordinate.latitude,
                longitude: $0.placemark.coordinate.longitude
            )
        }
        return  suggests
    }
}
```

今回はシングルトンなactorで実装してみました。
`requestSuggests(query:)`は、検索対象の文字列を受取り、サジェスト結果を配列で返す関数です。
実装手順としては3stepです。

#### step1: リクエスト内容を設定
```Swift
let localSearch = MKLocalSearch.Request()
localSearch.naturalLanguageQuery = query
```
まずは、上記の部分です。
`MKLocalSearch.Request`という型がMapKitから提供されています。
このクラスのプロパティに検索内容を設定することで様々な条件の地点情報を取得できます。
- naturalLanguageQuery: 文字列でサジェスト内容を絞り込めます
- addressFilter: 検索結果に含める、または除外する位置情報を設定できます
- pointOfInterestFilter: 検索結果煮含める、または除外するカテゴリを設定できます
- etc

#### step2: リクエストを実行
```Swift
let response = try? await MKLocalSearch(request: localSearch).start()
```
1行でサジェスト内容を取得できます。
今回はasync/awaitに対応したメゾットを利用しましたが、completion handlerのメゾットも用意されています。

#### step3: 整形
```Swift
guard let mapItems = response?.mapItems else { return [] }
let suggests = mapItems.compactMap {
    SuggestModel(
        name: $0.name ?? "",
        latitude: $0.placemark.coordinate.latitude,
        longitude: $0.placemark.coordinate.longitude
    )
}
return  suggests
```
リクエストの実行結果は`MKLocalSearch.Response`型です。
プロパティとして`mapItems`が存在し、ここを参照することで`MKMapItem`の配列を得ることができます。
`MKMapItem`は、サジェストされた位置情報の名前や緯度経度、その他様々な情報が入っています。
必要な情報を取り出し、整形してreturnしましょう！
あとはSuggestsModelをViewに反映すれば、位置情報サジェストを行いことができます！

## まとめ
MapKitは地図の描画以外にも地点情報の取得などを簡単に実装できる、非常に便利なフレームワークです。
外部APIと比べると精度や網羅性で劣る部分もありますが、外部サービスに依存せず無料で利用できることが最大の利点かと思います。
是非利用してみてください！