NCMBのSwift SDKを使って地図検索アプリを作る（その3：位置情報検索を実装する）

NCMBでは公式SDKとしてSwift/Objective-C/Kotlin/Java/Unity/JavaScript SDKを用意しています。また、それ以外にもコミュニティSDKとして、非公式ながらFlutter/React Native/Google Apps Script/C#/Ruby/Python/PHPなど幅広い言語向けにSDKが開発されています。

今回は公式SDKの一つ、Swift SDKを使って地図検索アプリを作ってみます。[前回は位置情報をデータストアにインポートする流れを解説](https://qiita.com/goofmint/items/f114dbc1264e0face3e3)しました。今回は位置情報検索と、その結果を地図上に反映する流れを解説します。

## 完成版のコード

作成したデモアプリのコードは[NCMBMania/swift_map_app](https://github.com/NCMBMania/swift_map_app)にアップロードしてあります。

## MapKitの追加

iOSで地図を扱う際にはMapKitを利用すると手軽です。Capabilitiesの中でMapKitを追加します。

![スクリーンショット 2022-10-18 11.10.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/40f81bc8-0a6f-3f8a-30b9-4ddfc90b81a8.png)


## 地図画面について

地図画面はメイン画面のタブで `StationView` として読み込まれています。

```swift
var body: some View {
		TabView {
				// 地図画面
				StationView()
						.tabItem {
								VStack {
										Image(systemName: "map")
										Text("地図")
								}
				}.tag(1)
				// インポート画面
				ImportView()
						.tabItem {
								VStack {
										Image(systemName: "gear")
										Text("Import")
								}
				}.tag(2)
		}
}
```

## UIについて

StationViewは地図画面であるMapViewを読み込んで、さらにマーカー消去用のフローティングボタンを配置しています。

```swift
var body: some View {
		ZStack {
				MapView(locations: $tapLocations, stattions: $stations) { location in
						self.selectedLocation = location
						self.showingAlert = true
				}
				VStack {
						Spacer()
						HStack {
								Spacer()
								Button(action: _removeMarkers, label: {
										Image(systemName: "trash")
												.foregroundColor(.white)
												.font(.system(size: 24)) // --- 4
								})
								.frame(width: 60, height: 60)
								.background(Color.orange)
								.cornerRadius(30.0)
								.shadow(color: .gray, radius: 3, x: 3, y: 3)
								.padding(EdgeInsets(top: 0, leading: 0, bottom: 16.0, trailing: 16.0)) // --- 5
								
						}
				}
		}
		// 地図をタップされた際に呼ばれるイベント
		.onChange(of: showingAlert) { value in
				// フラグを落とした際にも呼ばれるので、二重処理防止用
				if showingAlert == false {
						return
				}
				// タップされた位置情報を取得
				if let location = selectedLocation {
						// 位置情報を追加
						_addLocation(location: location)
						// フラグを落とす
						showingAlert = false
				}
		}
}
```

![Simulator Screen Shot - iPhone 14 - 2022-10-18 at 21.49.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/14f4713f-d77d-7ff4-b877-eb23a76d83b8.png)


MapKitはタップした際のイベントハンドリングが出来ないため、別途 `TapplableMapView` と `MapView` を使っています。これは [SwiftUIでマップを表示し、タップした座標にピンを立てる](https://gist.github.com/takoikatakotako/aca9ff14ee01baaf19128ad075ce4610) を参考にさせてもらっています。どちらも `MapView.swift` にて定義しています。

Mapの初期化、および初期表示の中心点は `TapplableMapView` にて定義しています。

```swift
override init(frame: CGRect) {
		super.init(frame: frame)
		
		let tapGestureRecognizer = UITapGestureRecognizer()
		tapGestureRecognizer.addTarget(self, action: #selector(onTap(sender:)))
		// 初期表示の中心点
		let coordinate = CLLocationCoordinate2DMake(35.6585805, 139.7454329)
		// 拡大率
		let span = MKCoordinateSpan(latitudeDelta: 0.065, longitudeDelta: 0.065)
		// 中心を設定
		let region = MKCoordinateRegion(center: coordinate, span: span)
		mapView.setRegion(region, animated: false)
		// ジェスチャー認識用
		mapView.addGestureRecognizer(tapGestureRecognizer)
		mapView.delegate = self
		self.backgroundColor = .red
		addSubview(mapView)
}
```

マーカーは `StationView` の `tapLocations` と `stations` にデータを追加すると、画面に反映されます。

```swift
struct StationView: View {
    // タップされた際にtrueになる
    @State var showingAlert = false
    // タップされた場所が入ってくる
    @State var selectedLocation: CLLocationCoordinate2D?
    // タップされた場所（最大2つ）が入る
    @State var tapLocations: [CLLocationCoordinate2D] = []
    // 検索結果の駅一覧の位置情報が入る配列
    @State var stations: [CLLocationCoordinate2D] = []
```

そして、画面をタップすると `showingAlert` が true になるので、それを捕捉して処理を行っています。

```swift
// 地図をタップされた際に呼ばれるイベント
.onChange(of: showingAlert) { value in
		// フラグを落とした際にも呼ばれるので、二重処理防止用
		if showingAlert == false {
				return
		}
		// タップされた位置情報を取得
		if let location = selectedLocation {
				// 位置情報を追加
				_addLocation(location: location)
				// フラグを落とす
				showingAlert = false
		}
}
```

## タップした際の処理

地図をタップした際には `_addLocation` が実行されます。この時、1回目のタップだった時にはその付近の駅を、2回目以降のタップだった場合には1回目のタップしたマーカーと合わせて矩形検索を行います。

```swift
// タップされた際に呼ばれる処理
private func _addLocation(location: CLLocationCoordinate2D) {
		// すでに2つマーカーがあるかどうか
		if tapLocations.count == 2 {
				// あれば最初のマーカーを消して、新しいマーカーを1つ目にする
				tapLocations[0] = tapLocations[1]
				tapLocations.remove(at: 1)
		}
		// タップした場所を追加
		tapLocations.append(location)
		// マーカーの数によって検索条件を変える
		if tapLocations.count == 2 {
				// 矩形検索
				_geoBoxSearch()
		} else {
				// 近距離検索
				_nearBySearch()
		}
}
```

付近の駅を検索する `_nearBySearch` 関数は以下のようになります。

```swift
// タップしたマーカーの付近にある駅を検索する
private func _nearBySearch() {
		// 検索対象のデータストアのクラス（DBで言うテーブル名相当）
		var query = NCMBQuery.getQuery(className: "Station")
		// 検索条件になる位置情報を作成
		let geo = NCMBGeoPoint(latitude: selectedLocation!.latitude, longitude: selectedLocation!.longitude)
		// nearGeoPointで検索を行う
		query.where(field: "geo", nearGeoPoint: geo,
								withinKilometers: 3.0)
		// 検索実行
		query.findInBackground(callback: { result in
				// 検索処理がうまくいっていれば、駅を配列に登録する
				if case let .success(ary)  = result {
						_appendStations(ary: ary)
				}
		})
}
```

![Simulator Screen Shot - iPhone 14 - 2022-10-18 at 21.48.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/6a51dbe1-d785-2bce-5b9b-75e2fc0ef513.png)


2点による矩形検索処理は以下のようになります。

```swift
// 二点間の矩形検索を行う
private func _geoBoxSearch() {
		// 検索対象のデータストアのクラス（DBで言うテーブル名相当）
		var query = NCMBQuery.getQuery(className: "Station")
		// 二点の位置情報オブジェクトを作成
		let geo1 = NCMBGeoPoint(latitude: tapLocations[0].latitude, longitude: tapLocations[0].longitude)
		let geo2 = NCMBGeoPoint(latitude: tapLocations[1].latitude, longitude: tapLocations[1].longitude)
		// withinGeoBoxFromSouthwestで検索を行う
		query.where(field: "geo", withinGeoBoxFromSouthwest: geo1, toNortheast: geo2)
		// 検索実行
		query.findInBackground(callback: { result in
				// 検索処理がうまくいっていれば、駅を配列に登録する
				if case let .success(ary)  = result {
						_appendStations(ary: ary)
				}
		})
}
```

![Simulator Screen Shot - iPhone 14 - 2022-10-18 at 21.48.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/2827fba8-705d-c0b9-3327-b5d87d9f0f12.png)


どちらも検索結果の駅一覧を `_appendStations` を使ってマーカーに反映します。

```swift
private func _appendStations(ary: [NCMBObject]) {
		stations.removeAll()
		ary.forEach { station in
				let geo = station["geo"]! as NCMBGeoPoint
				let location = CLLocationCoordinate2D(latitude: geo.latitude, longitude: geo.longitude)
				stations.append(location)
		}
}
```

後はマーカーを削除する処理として `_removeMarkers` 関数を用意しています。

```swift
// すべてのマーカーを削除する処理
private func _removeMarkers() {
		tapLocations.removeAll()
		stations.removeAll()
}
```

これで地図検索アプリの完成です。

## まとめ

NCMBを使えば位置情報を使った検索も簡単に実装できます。スマートフォンアプリでは位置情報をよく使いますので、ぜひNCMBの位置情報機能を役立ててください。

[mBaaSでサーバー開発不要！ | ニフクラ mobile backend](https://mbaas.nifcloud.com/)