NCMBのSwift SDKを使って地図検索アプリを作る（その2：位置情報をインポートする）

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を使って地図検索アプリを作ってみます。[前回は画面の仕様とSDKの初期化について解説しました](https://qiita.com/goofmint/items/102ccc9d4242d18903b8)。今回は位置情報をデータストアにインポートする流れを解説します。

## 完成版のコード

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

## インポート画面について

インポート画面はContentViewのタブで読み込まれています。 `ImportView` がそうです。

```swift
// インポート画面
ImportView()
		.tabItem {
				VStack {
						Image(systemName: "gear")
						Text("Import")
				}
}.tag(2)
```

## インポート画面の構築

まず `ImportView` のStateについてです。文字列の配列が入る logs と データをインポート（または削除）するNCMBのデータストアクラス名を入れた _className があります。

```swift
struct ImportView: View {
    @State private var logs: [String] = []
    private var _className = "Station"
```

UIは次のようになります。インポート実行タンをタップすると `_execute` が実行されます。また、logs が更新されると List を使ってログが表示されます。

```swift
var body: some View {
		VStack(spacing: 16
		) {
				Text("駅一覧を読み込みます").padding()
				// ボタンを押したら駅登録処理開始
				Button(action: _execute, label: { Text("インポート実行")})
				if logs.count > 0 {
						// ログ表示用
						Text("ログ")
						List {
								ForEach(Array(logs.enumerated()), id: \.element) { index, log in
										Text(logs[index])
								}
						}
				}
		}
}
```


![Simulator Screen Shot - iPhone 14 - 2022-10-18 at 21.49.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/c4f22c46-9000-7a2c-5cb7-a926fa28db0d.png)


## データのインポートについて

データをインポートする際には繰り返し処理できるように、まず既存データを削除しています。もし既存データがなかった場合でも（データストアのクラスがなくとも）エラーにはならず、空の配列が返ってくるだけです。

```swift
private func _execute() {
		// まずデータを削除する
		_removeStations()
		// JSONファイルを読み込む
		let stations = _loadStations()
		// JSONファイルの内容をデータストアに登録する
		registerDataStore(stations: stations)
}
```

`_removeStations` は以下のようになります。NCMBQueryを使ってデータを取得し、各データ（NCMBObject）のdeleteメソッドを使ってデータを削除します。

```swift
// すでにデータストアにある駅名データを削除する処理（何度も繰り返せる用）
private func _removeStations() {
		// 検索対象のデータストアのクラス（DBで言うテーブル名相当）
		var query = NCMBQuery.getQuery(className: _className)
		// 100件対象とする
		query.limit = 100
		// 検索実行
		query.findInBackground(callback: { result in
				// 処理が成功している場合
				if case let .success(ary)  = result {
						// すべてのデータを削除
						ary.forEach{ station in
								// 削除処理（非同期）
								station.deleteInBackground(callback: {_ in
										// 特に処理なし
								})
						}
				}
		})
}
```

データを削除したら、山手線の各駅の位置情報が入ったJSONファイルを読み込みます。

```swift
// アセットにあるJSONファイルを読み込む
private func _loadStations() -> [Station] {
		// インポートする
		guard let url = Bundle.main.url(forResource: "yamanote", withExtension: "json") else {
				fatalError("ファイルが見つからない")
		}
		// 読み込み
		guard let data = try? Data(contentsOf: url) else {
				fatalError("ファイル読み込みエラー")
		}
		// JSONデコード
		let decoder = JSONDecoder()
		guard let stations = try? decoder.decode([Station].self, from: data) else {
				fatalError("JSON読み込みエラー")
		}
		return stations
}
```

この時利用している `Station` という構造体は上記のようになっています。

```swift
struct Station: Codable {
    var name: String
    var latitude: Double
    var longitude: Double
}
```

読み込んだデータをデータストアに登録する `registerDataStore` 関数は以下のようになります。JSONファイルのパラメータを渡して、Stationクラスにデータを作成します。NCMBGeoPointを使って位置情報を保存するのがポイントです。

```swift
// 駅名の配列をデータストアに登録する
private func registerDataStore(stations: [Station]) {
		stations.forEach{ params in
				// 登録するデータストアのクラス（DBで言うテーブル名相当）
				let station = NCMBObject(className: _className)
				// 駅名をセット
				station["name"] = params.name
				// 位置情報はNCMBGeoPointを利用する
				let geo = NCMBGeoPoint(latitude: params.latitude, longitude: params.longitude)
				// 位置情報をセット
				station["geo"] = geo
				// 保存処理（非同期処理）
				station.saveInBackground(callback: { result in
						// 成功していればログに追記
						if case .success(_)  = result {
								let log = "保存しました -> \(params.name)"
								logs.append(log)
						}
				})
		}
}
```

![Simulator Screen Shot - iPhone 14 - 2022-10-18 at 21.49.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/3d74c216-a014-598a-70f5-ff36b4574780.png)


## 全体のコード

ImportViewの内容は次のようになります。

```swift
struct Station: Codable {
    var name: String
    var latitude: Double
    var longitude: Double
}

struct ImportView: View {
    @State private var logs: [String] = []
    private var _className = "Station"
    
    // すでにデータストアにある駅名データを削除する処理（何度も繰り返せる用）
    private func _removeStations() {
        // 検索対象のデータストアのクラス（DBで言うテーブル名相当）
        var query = NCMBQuery.getQuery(className: _className)
        // 100件対象とする
        query.limit = 100
        // 検索実行
        query.findInBackground(callback: { result in
            // 処理が成功している場合
            if case let .success(ary)  = result {
                // すべてのデータを削除
                ary.forEach{ station in
                    // 削除処理（非同期）
                    station.deleteInBackground(callback: {_ in
                        // 特に処理なし
                    })
                }
            }
        })
    }
    
    // アセットにあるJSONファイルを読み込む
    private func _loadStations() -> [Station] {
        // インポートする
        guard let url = Bundle.main.url(forResource: "yamanote", withExtension: "json") else {
            fatalError("ファイルが見つからない")
        }
        // 読み込み
        guard let data = try? Data(contentsOf: url) else {
            fatalError("ファイル読み込みエラー")
        }
        // JSONデコード
        let decoder = JSONDecoder()
        guard let stations = try? decoder.decode([Station].self, from: data) else {
            fatalError("JSON読み込みエラー")
        }
        return stations
    }
    
    // 駅名の配列をデータストアに登録する
    private func registerDataStore(stations: [Station]) {
        stations.forEach{ params in
            // 登録するデータストアのクラス（DBで言うテーブル名相当）
            let station = NCMBObject(className: _className)
            // 駅名をセット
            station["name"] = params.name
            // 位置情報はNCMBGeoPointを利用する
            let geo = NCMBGeoPoint(latitude: params.latitude, longitude: params.longitude)
            // 位置情報をセット
            station["geo"] = geo
            // 保存処理（非同期処理）
            station.saveInBackground(callback: { result in
                // 成功していればログに追記
                if case .success(_)  = result {
                    let log = "保存しました -> \(params.name)"
                    logs.append(log)
                }
            })
        }
    }
    
    private func _execute() {
        // まずデータを削除する
        _removeStations()
        // JSONファイルを読み込む
        let stations = _loadStations()
        // JSONファイルの内容をデータストアに登録する
        registerDataStore(stations: stations)
    }
    
    var body: some View {
        VStack(spacing: 16
        ) {
            Text("駅一覧を読み込みます").padding()
            // ボタンを押したら駅登録処理開始
            Button(action: _execute, label: { Text("インポート実行")})
            if logs.count > 0 {
                // ログ表示用
                Text("ログ")
                List {
                    ForEach(Array(logs.enumerated()), id: \.element) { index, log in
                        Text(logs[index])
                    }
                }
            }
        }
    }
}
```

## まとめ

今回はJSONファイルをインポートし、それを使ってNCMBObjectを作成しました。[次回は地図コンポーネントと合わせて、位置情報検索を実装](https://qiita.com/goofmint/items/b7d4bfec50a51de603d4)します。