はじめに
Combineを使った非同期処理として、URLSession
は標準ライブラリが提供されておりますが、Firestore
は自分で実装する必要があります。
このあたりの日本語での情報量が、URLSession
と比較して、未開拓に感じましたので、自分が実装したサンプルコードを記事にしてみました。
今回、書いた処理は、Firestoreからの取得・追加・削除の処理です。(CRUDの中では更新がありませんが、更新処理は追加処理とほぼ変わらないため割愛します。)
追記(2022/03/01)
@GleamingCake さんからコメント頂き、知ったのですが、公式リポジトリ内でコミュニティベースのCombine
をサポートするAPIが実装されているようです。今回の記事で私が実装した方法とは少し異なりますが、同じユースケースが実現可能なメソッドがありましたので、リンクを載せさせていただきます。
なかでも本記事と同じユースケースで使うメソッドがあるのは、
DocumentReference+Combine.swift
とQuery+Combine.swift
の2つのファイルです。
本記事の内容を理解していただければ、似たような処理なので、上記のライブラリの使い方もわかると思います。
前提条件
-
FirebaseFirestore
とFirebaseFirestoreSwift
がプロジェクトに導入されていること - ジェネリクスについて理解していること
-
Combine
とFirestore
の使い方を理解していること
ジェネリクスについて
Firestoreについて
Combineについて
実装
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
を導入しなくても使えるので、幅広いアプリ開発において導入の検討が可能だと思います。
おわりに
今回載せたコードはコピペでも最低限動くと思いますが、エラーハンドリングはアプリごとに異なる実装が必要ですので、こちらをベースに、皆様のアプリ開発の参考になれば幸いです。
参考にさせていただいた資料