【Swift】Firestore × Codableの通信を共通化する

## はじめに
今回は、Firestoreに対してデータをリクエストする処理（通信処理〜エラーハンドリング）を共通化してみたので、自分用のスニペット的な意味も込めて記事にまとめてみました。

今回は、API通信を共通化しているこちらの記事からヒントを得てそれをFirestoreに適用しました。

https://blog.shota-ito.com/2020/06/26/post-579/

また、Firestoreで取得したデータをCodableでデコードする処理については、自分が以前記事にしたものがありますので、そちらをご覧いただければと思います。

https://qiita.com/ctnc_67/items/7027a10c8c18014fddfa

## 前提条件となる知識
- Firestoreについての基礎知識（クエリの書き方など）
- Codableを使ったDecode処理
- ジェネリクスについての基礎知識

## 実装

まずは、リクエストを送信するためのクラスを作ります。
多くの場合は、RequestとResponseの関係は1:1だと思うので、RequestクラスにCodableに準拠したデータモデルのクラスを紐付けるような形で定義します。

```swift
protocol FirestoreRequest {
    associatedtype Response: Codable
}

// 具象クラスはこんな感じ
struct ProductListRequest: FirestoreRequest {
    typealias Response = ProductDetail
    // コレクション名はここに定義しておく
    let product: String = "product"
}
```

`ProductDetail`クラスの持っているプロパティは、今回の説明とはあまり関係ないので省略します。
Codableを継承していればOKです。

1番重要な、Firestoreとの通信を共通で利用できるように定義したクラスがこちらです。

```swift
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift

class FirestoreClient {
    func fetch<Request: FirestoreRequest>(request: Request, ref: Query, completion: @escaping (Result<[Request.Response], Error>) -> Void) {
        ref.getDocuments { snapShot, error in
            if let error = error {
                completion(Result(error: FirestoreError.undefinedError(error)))
            } else {
                guard let snapShot = snapShot else {
                    completion(Result(error: FirestoreError.dataNotFoundError))
                    return
                }
                do {
                    var list = [Request.Response]()
                    for doc in snapShot.documents {
                        let item = try self.decode(type: Request.Response.self, data: doc.data(), ref: doc.reference)
                        list.append(item)
                    }
                    completion(Result(model: list))
                } catch let error as FirestoreDecodeError {
                    completion(Result(error: FirestoreError.decoderError(error)))
                } catch let error {
                    completion(Result(error: FirestoreError.undefinedError(error)))
                }
            }
        }
    }
    
    private func decode<T: Codable>(type: T.Type, data: [String: Any], ref: DocumentReference?) throws -> T {
        do {
            return try Firestore.Decoder().decode(T.self, from: data, in: ref)
        } catch {
            throw FirestoreDecodeError()
        }
    }
}
```

`func send<Request: FirestoreRequest>(request: Request, ref: Query, completion: @escaping (Result<[Request.Response], Error>) -> Void)`のところがポイントですが、Requestするクラスを選択した時点で、Responseとして返却する型も決まるような仕組みになっています。

Firestoreのレスポンスは、配列として帰ってくるので、通信完了後に返却する時は、レスポンスのArray型になります。

次にエラーハンドリングのためのエラーを定義しますが、これは要件によって変更が必要と思いますが、ご参考までに。

```swift
enum FirestoreError: Error {
    case decoderError(Error)
    case dataNotFoundError
    case undefinedError(Error?)
}

struct FirestoreDecodeError: Error {}
```

結果を返すためのクラスはこんな感じで定義しました。

```swift
enum Result<T, Error> {
    case success(T)
    case failure(Error)
    
    init(model: T) {
        self = .success(model)
    }
    
    init(error: Error) {
        self = .failure(error)
    }
}
```

データ取得のリクエストを投げる時はこんな感じです。

```swift
import Foundation
import FirebaseFirestore

class FirestoreService {
    
    public static let shared = FirestoreService()

    private init() {}
    
    let client = FirestoreClient()
    
    func requestProductList(completion: @escaping (_ model: [ProductDetail]?, _ error: Error?) -> Void) {
        let request = ProductListRequest()
        // refはQuery型であれば良いので、.order(by: String)などで、並び順を指定してフェッチしてもOK！
        let ref: CollectionReference = Firestore.firestore().collection(request.product)
        client.fetch(request: request, ref: ref) { result in
            switch result {
            case .success(let model):
                completion(model, nil)
            case .failure(let error):
                completion(nil, error)
            }
        }
    }
```

## おわりに
今回は、商品一覧の取得ですが、通信処理のところを共通化したことによって、他のデータの取得も簡単に追加できると思います。

Firestoreでアプリを作ろうとしている方は、是非試してみてください。

多分、「追加」『更新」「削除」も共通化できそう。。。