【Swift】Firestore × Codable 〜取得データを簡単にデコード〜

最近、iOSアプリ開発で`Cloud Firestore`を使っているのですが、何も気にせず使っていたら1つ気になることが、、、
**Firestoreから取得したデータのデコードがめんどくさい**
WebAPIからJSONで取ってきたデータは、Codableで簡単にデコードできるので、Firestoreから取ってきたデータも簡単にデコードしたい！と思い調べてみたので、記事にしました。
Codableについては、こちらの記事が大変参考になるかと思います。
https://qiita.com/UJIPOID/items/2c436a80f1167f7bcac0

## リファクタリング前のコード

`Firestore` × `Codable`を導入する前のコードをこちらに貼っておきます。

```swift
import FirebaseFirestore

// MARK: - Menu
struct Menu: Codable {
    let id: String
    let category: String
    let imageName: String
    let name: String
    let price: Int
}

// Firestoreからデータ全件取得＆デコード
func fetchMenuData() {
    let db = Firestore.firestore()
    // Firestoreからメニューデータを取得
    db.collection("menu").getDocuments { (_snapShot, _error) in
        if let snapShot = _snapShot {
            let menuList = snapShot.documents.map { menu -> Menu in
                let data = menu.data()
                // デコード開始（ここがスッキリしない）
                let id: String = data["id"] as! String
                let category: String = data["category"] as! String
                let imageName: String = data["imageName"] as! String
                let name: String = data["name"] as! String
                var price = 0
                if let priceData = data["price"] {
                    if let priceInt = Int(String(describing: priceData)) {
                        price = priceInt
                    }
                }
                return MenuDetail(id: id, category: category, imageName: imageName, name: name, price: price)
            }
            // 取得データを出力
            print(menuList)
        } else {
            print("Data Not Found")
        }
    }
}
```

ざっとこんな感じです。

やろうとしていることとしては、Firestoreに入っているデータを全件取得する処理です。
JSONをCodableを使ってデコードすることに慣れてしまっていると、少し冗長に感じます。
構造体のプロパティ数が増えたらもっと長くなってしまいますね。

**というわけで、早速リファクタリングしていきましょう。**

## FirebaseFirestoreSwift を導入する
■ FirebaseFirestoreSwiftについて
Firestoreから取得したデータのデコードを簡単にやってくれるライブラリです。
https://cocoapods.org/pods/FirebaseFirestoreSwift

とりあえず導入してみる

```console
pod 'Firebase/Firestore'
pod 'FirebaseFirestoreSwift'
```

これで、`pod install`を実行すればOKなはずです。

## Firestore × Codable で実装
ちなみに、Firestoreからの取得データをクエリで指定して1件のみ取得したい方はこちらの公式リファレンスをご覧ください。（多分こっち見たほうが早いです）
https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja
→ 場所分かりづらいですが、ページ検索で「FirebaseFirestoreSwift」を検索すれば出てきます。

**リファクタリングした結果**

```swift

import FirebaseFirestore
import FirebaseFirestoreSwift

--- 省略 --- 

// MARK: - Menu
struct Menu: Codable {
    let id: String
    let category: String
    let imageName: String
    let name: String
    let price: Int
}

// Firestoreからデータ全件取得＆デコード
func fetchMenuData() {
    let db = Firestore.firestore()
    // Firestoreからメニューデータを取得
    db.collection("menu").getDocuments { (_snapShot, _error) in
        if let snapShot = _snapShot {
            let documents = snapShot.documents
            let menuList = documents.compactMap {
                // この1行でデコード終了
                return try? $0.data(as: Menu.self)
            }
            // 取得データを出力
            print(menuList)
        } else {
            print("Data Not Found")
        }
    }
}
```

ポイントは、ここです。

```swift
let menuList = documents.compactMap {
    // この1行でデコード終了
    return try? $0.data(as: Menu.self)
}
```
リファクタリング前のコードと比較すると、明らかに簡単にデコードできていることがお分かりいただけると思います。
リファクタリング前は、`data["KEY"]`の形式で、値を1件ずつ取り出していましたが、「FirebaseFirestoreSwift」を使うことで、その必要がなくなりました。

余談ですが、今回は複数件取得したデータをデコードする際に`compactMap(_:)`を使いました。
`try? $0.data(as: Menu.self)`が失敗した場合は、`nil`が帰ってきますので、その場合は特にエラーハンドリングをせずに、配列に含めないという操作を1行で簡単に実装できます。

`compactMap(_:)`についてはこちら。
https://qiita.com/yum_fishing/items/7592a56f7e9dbeef8e75

ちなみに公式リファレンスのやり方をほぼ参考にしていますが、デコードに失敗した場合に、処理を行いたい場合はこのようにすれば良いと思います。
エラー処理適当です🙏

```swift
let menuList = documents.compactMap { doc in
                    
    let result = Result {
        try doc.data(as: Menu.self)
    }
    
    switch result {
    case .success(let _menu):
        if let menu = _menu {
            return menu
        } else {
            print("Document does not exist")
            return nil
        }
    case .failure(let error):
        print("Error decoding menu: \(error.localizedDescription)")
        return nil
    }
}
```

## 参考にさせていただいた記事・サイト
https://qiita.com/tsubasa_hiroe/items/43b3552975d794383881
https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja