【Swift】Cloud Firestoreでmap型を取得するTips

## 修正（02/14）
2つ目の方法は、`FirebaseFirestoreSwift`を使わないとできないというのは自分の勘違いでした。すみません。
現在は、修正しております。

## はじめに
最近、Firestoreを使ったデモアプリを作って遊んでおります。
Firestoreでサポートされているデータ型は色々あるのですが、今回はその中でも**map型**のデータを取得する方法について書きました。
Firestoreでサポートされるデータ型についてはこちらの公式リファレンスをご覧ください。
https://firebase.google.com/docs/firestore/manage-data/data-types

今回は、2つの方法を載せておきます。

## 動作環境
- Xcode12.4
- iOS14.4

## 今回取得するFirestoreのデータ＆Swift側のオブジェクト
Firestoreのmap型を取得するということで、Firestore側のデータ構造があったほうがわかりやすいと思いましたので、載せておきます。

■ Firestoreのmap型データモデル
![FirestoreData.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580445/8da9c058-46cb-9953-7195-3dbf3504f740.png)

■ Swift側のオブジェクト（構造体）

```swift:Order.swift
struct Order: Codable {
    let id: String
    let orderNo: Int
    let imageName: String
    let name: String
    let price: Int
    let isWasabi: Bool
    var isOrderComplete: Bool
}
```

## [String : Any]から直接インスタンスを生成する方法

```swift
func fetchOrderMap_A() {
    db.collection("ordering").document("01").getDocument { (_document, _error) in
        guard _error == nil else {
            print("Firestoreからエラーを受け取りました。")
            return
        }
        if let document = _document, document.exists {
            
            // document.data()は、[String : Any]になる
            let mapData = document.data() ?? [:]
            
            print(mapData)  // ※補足参照
            
            var orderList: [Order] = []
            // データを1件ずつ抽出
            for (_, data) in mapData {
                // data(Any型)をdicData([String: Any]型)にキャストする <- Point
                let dicData = data as! [String : Any]
                // [String: Any]にキャストされていることを確認
                print(dicData)
                // key - value でプロパティ別に値を取り出す。（今回は強制アンラップ。Firestoreのデータ型に合わせてキャストすれば問題なし。）
                let id = dicData["id"] as! String
                let orderNo: Int = dicData["orderNo"] as! Int
                let imageName: String = dicData["imageName"] as! String
                let name: String = dicData["name"] as! String
                let price: Int = dicData["price"] as! Int
                let isWasabi: Bool = dicData["isWasabi"] as! Bool
                let isOrderComplete: Bool = dicData["isOrderComplete"] as! Bool
                // インスタンス生成して配列に追加
                let order = Order(id: id, orderNo: orderNo, imageName: imageName, name: name, price: price, isWasabi: isWasabi, isOrderComplete: isOrderComplete)
                orderList.append(order)
            }
            // 生成された配列を出力
            print(orderList)
        } else {
            print("ドキュメントが存在しません。")
        }
    }
}
```

### ※補足
`print(mapData)`実行時の`mapData`の中身はこんな感じです。

```console:補足
["2": {
    id = 002;
    imageName = samon;
    isOrderComplete = 1;
    isWasabi = 1;
    name = "\U30b5\U30fc\U30e2\U30f3";
    orderNo = 2;
    price = 150;
}, "1": {
    id = 001;
    imageName = "maguro_akami";
    isOrderComplete = 0;
    isWasabi = 0;
    name = "\U8d64\U8eab\U30de\U30b0\U30ed";
    orderNo = 1;
    price = 100;
}]
```
上記のようにkey,valueの配列になっているので、`for文`で1件ずつ処理していきます。
データを1件取得できたら、取得したいデータ部を`[String : Any]`でキャストします。
現時点では、`name`がUnicodeになっていますが問題ありません。

このようなステップを踏むことで、Swiftから容易に扱える形にしています。
ここまで実装できれば、後は通常のJSON解析などとやり方は同じだと思います。


## JSONを噛ませてからCodableを使う方法

```swift
func fetchOrderMap_B() {
    db.collection("ordering").document("01").getDocument { (_document, _error) in
        guard _error == nil else {
            print("Firestoreからエラーを受け取りました。")
            return
        }
        if let document = _document, document.exists {
            
            // document.data()は、[String : Any]になる
            let mapData = document.data() ?? [:]
            
            print(mapData)  // ※補足参照
            
            var orderList: [Order] = []
            // データを1件ずつ抽出
            for (_, data) in mapData {
                // data(Any型)をdicData([String: Any]型)にキャストする <- Point
                let dicData = data as! [String : Any]
                // [String: Any]にキャストされていることを確認
                print(dicData)
                // ここからJSONを利用する場合の固有処理
                // JSON へ一時的にキャスト
                let jsonData = try? JSONSerialization.data(withJSONObject: dicData)
                // JSON -> Order(Codable)
                let order = try? JSONDecoder().decode(Order.self, from: jsonData!)
                if let order = order {
                    orderList.append(order)
                }
            }
            // 生成された配列を出力
            print(orderList)
        } else {
            print("ドキュメントが存在しません。")
        }
    }
}
```
処理内容はコメントに書かせていただきましたが、ポイントは
`[String:Any]` -> `JSON` -> `Order` というように一時的に`JSON`を噛ませてから、目的の`Order`に辿り着いていることです。
自分はこの方法しか思いつかなかったのですが、もし他にいい方法がございましたらコメントにて教えていただけると嬉しいです。

## まとめ
最初実装していたときは、Firestoreでmap型を取得したら、何も考えずにデコードできると思っていましたが、`id = 002;`のような形式で値が帰ってきたので最初戸惑いました。

iOSアプリ開発時にFirestoreのmap型を使いたいという方は参考にしていただけると幸いです。