【Swift】Combine ✕ Firestore ✕ Codableで通信処理を書く（取得・追加・削除の処理）

## はじめに
Combineを使った非同期処理として、`URLSession`は標準ライブラリが提供されておりますが、`Firestore`は自分で実装する必要があります。

このあたりの日本語での情報量が、`URLSession`と比較して、未開拓に感じましたので、自分が実装したサンプルコードを記事にしてみました。

今回、書いた処理は、Firestoreからの**取得・追加・削除**の処理です。（CRUDの中では更新がありませんが、更新処理は追加処理とほぼ変わらないため割愛します。）

## 追記(2022/03/01)
@GleamingCake さんからコメント頂き、知ったのですが、公式リポジトリ内でコミュニティベースの`Combine`をサポートするAPIが実装されているようです。今回の記事で私が実装した方法とは少し異なりますが、同じユースケースが実現可能なメソッドがありましたので、リンクを載せさせていただきます。

https://github.com/firebase/firebase-ios-sdk/tree/master/FirebaseCombineSwift 

なかでも本記事と同じユースケースで使うメソッドがあるのは、
`DocumentReference+Combine.swift`と`Query+Combine.swift`の2つのファイルです。

本記事の内容を理解していただければ、似たような処理なので、上記のライブラリの使い方もわかると思います。

## 前提条件
- `FirebaseFirestore`と`FirebaseFirestoreSwift`がプロジェクトに導入されていること
- ジェネリクスについて理解していること
- `Combine`と`Firestore`の使い方を理解していること

#### ジェネリクスについて
https://swift.codelly.dev/guide/ジェネリクス/#ジェネリック型-generic-types

#### Firestoreについて
https://firebase.google.com/docs/firestore?hl=ja

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

#### Combineについて
https://developer.apple.com/documentation/combine

https://qiita.com/ctnc_67/items/394e668ee8c2a0b3327a

## 実装
```swift:Firestore+Extension.swift
import Foundation
import Combine
import FirebaseFirestore
import FirebaseFirestoreSwift

// MARK: Firestore+Combine
extension Firestore {
    /// 取得
    func fetch<T: Codable>(ref: Query) -> AnyPublisher<[T], Error> {
        return Future<[T], Error> { promise in
            ref.getDocuments { snapshot, error in
                if let error = error {
                    promise(.failure(error))
                    return
                }
                if let snapshot = snapshot {
                    do {
                        let entities: [T] = try snapshot.documents.compactMap {
                            try Self.Decoder().decode(T.self, from: $0.data(), in: $0.reference)
                        }
                        promise(.success(entities))
                    } catch(let error) {
                        promise(.failure(error))
                    }
                }
            }
        }.eraseToAnyPublisher()
    }

    /// 追加
    func add<T: Codable>(ref: DocumentReference, data: T) -> AnyPublisher<Void, Error> {
        return Future<Void, Error> { [weak self] promise in
            guard let `self` = self else { return }
            
            do {
                let encodeData = try self.encode(data: data)
                ref.setData(encodeData) { error in
                    
                    if let error = error {
                        promise(.failure(error))
                        return
                    }
                    promise(.success(()))
                }
            } catch let error {
                promise(.failure(error))
            }
        }
        .eraseToAnyPublisher()
    }

    /// 削除
    func delete(ref: DocumentReference) -> AnyPublisher<Void, Error> {
        return Future<Void, Error> { promise in
            ref.delete { error in
                if let error = error {
                    promise(.failure(error))
                    return
                }
                promise(.success(()))
            }
        }
        .eraseToAnyPublisher()
    }
    
    private func encode<T: Codable>(data: T) throws -> [String: Any] {
        do {
            return try Firestore.Encoder().encode(data)
        } catch {
            throw FirestoreEncodeError()
        }
    }
}

// MARK: - Custom Error
struct FirestoreEncodeError: Error {}
```

## 使用例
```swift
// 取得の場合の例: 基本的に使い方は、追加・削除も同様となります。
func fetchTestUsers() -> AnyPublisher<[User], Error> {
    let query = db.collection("user")
        .document("testtest01")
        .collection("testUser")
        .order(by: "createdTime", descending: false)
    return db.fetch(ref: query)
}
```

## 解説
### Futureについて
今回のFirestoreとの通信処理には、すべて`Future`を利用しています。
`Future`は、これまでクロージャがになっていた、処理が完了したら`completion()`などを利用して呼び出し側に値を返すという処理を担当する`Combine`フレームワークに組み込まれたクラスです。

`Combine`を利用しない形で、`Firestore`のデータを非同期的に取得しようとすると、
```swift
func fetch<T: Codable>(ref: Query, completion: @escaping (Result<[T], Error>) -> Void){
    // 処理
}
```

のようなメソッドを利用して、通信処理をすることが想定されますが、

`Future`を利用することで、`AnyPublisher<[T], Error>`をメソッドの返り値として利用できるようになります。

上記のようにすることで、一般的に可動性を下げてしまうと言われる、クロージャのコールバック地獄を回避し、
なおかつModel層よりも上位の層で、Firestoreから受け取るべき値を購読（監視）できるメリットがあります。

追加と削除については、`Future<Void, Error>`となっていますが、`Void`を採用した理由として、
追加・削除については、処理の正常終了orエラーかどうかだけ知りたいので、今回は、特定の値を呼び出し元にリターンしないようにしています。

### Encode / Decode 処理について
Firestoreにデータを送る際にジェネリクスによる再利用可能なコードを実装するために、`Codalbe`の仕組みを利用しています。
`FirebaseFirestoreSwift`を使うことで実装可能になります。
こちらは、`Combine`を導入しなくても使えるので、幅広いアプリ開発において導入の検討が可能だと思います。

## おわりに
今回載せたコードはコピペでも最低限動くと思いますが、エラーハンドリングはアプリごとに異なる実装が必要ですので、こちらをベースに、皆様のアプリ開発の参考になれば幸いです。

## 参考にさせていただいた資料
https://github.com/rever-ai/CombineFirebase

https://yutaabe200.hatenablog.com/entry/2021/01/22/【SwiftUI】FirestoreのラッパーをCombineで書いてみた%5B備忘録%5D