LoginSignup
2

More than 1 year has passed since last update.

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

Posted at

はじめに

今回は、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でアプリを作ろうとしている方は、是非試してみてください。

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

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
2