12
5

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-02-26

はじめに

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

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

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

追記(2022/03/01)

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

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

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

前提条件

  • FirebaseFirestoreFirebaseFirestoreSwiftがプロジェクトに導入されていること
  • ジェネリクスについて理解していること
  • CombineFirestoreの使い方を理解していること

ジェネリクスについて

Firestoreについて

Combineについて

実装

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 {}

使用例

// 取得の場合の例: 基本的に使い方は、追加・削除も同様となります。
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のデータを非同期的に取得しようとすると、

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を導入しなくても使えるので、幅広いアプリ開発において導入の検討が可能だと思います。

おわりに

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

参考にさせていただいた資料

12
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
5