はじめに
今回は、Firestoreに対してデータをリクエストする処理(通信処理〜エラーハンドリング)を共通化してみたので、自分用のスニペット的な意味も込めて記事にまとめてみました。
今回は、API通信を共通化しているこちらの記事からヒントを得てそれをFirestoreに適用しました。
また、Firestoreで取得したデータをCodableでデコードする処理については、自分が以前記事にしたものがありますので、そちらをご覧いただければと思います。
前提条件となる知識
- Firestoreについての基礎知識(クエリの書き方など)
- Codableを使ったDecode処理
- ジェネリクスについての基礎知識
実装
まずは、リクエストを送信するためのクラスを作ります。
多くの場合は、RequestとResponseの関係は1:1だと思うので、RequestクラスにCodableに準拠したデータモデルのクラスを紐付けるような形で定義します。
protocol FirestoreRequest {
associatedtype Response: Codable
}
// 具象クラスはこんな感じ
struct ProductListRequest: FirestoreRequest {
typealias Response = ProductDetail
// コレクション名はここに定義しておく
let product: String = "product"
}
ProductDetail
クラスの持っているプロパティは、今回の説明とはあまり関係ないので省略します。
Codableを継承していればOKです。
1番重要な、Firestoreとの通信を共通で利用できるように定義したクラスがこちらです。
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型になります。
次にエラーハンドリングのためのエラーを定義しますが、これは要件によって変更が必要と思いますが、ご参考までに。
enum FirestoreError: Error {
case decoderError(Error)
case dataNotFoundError
case undefinedError(Error?)
}
struct FirestoreDecodeError: Error {}
結果を返すためのクラスはこんな感じで定義しました。
enum Result<T, Error> {
case success(T)
case failure(Error)
init(model: T) {
self = .success(model)
}
init(error: Error) {
self = .failure(error)
}
}
データ取得のリクエストを投げる時はこんな感じです。
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でアプリを作ろうとしている方は、是非試してみてください。
多分、「追加」『更新」「削除」も共通化できそう。。。