LoginSignup
2

More than 3 years have passed since last update.

[Swift]Firestoreのdocument取得を見やすく書けるようにしてみる

Last updated at Posted at 2020-12-01

アドベントカレンダー1日目! 一番いい場所いただきました! :santa: :santa: :santa: :santa: :santa:

内容

Firestoreのドキュメントでは、データの取得の際に以下のようなコードが紹介されています.
ドキュメントはとってもわかりやすいですが、データ取得の際、毎回こう書くのは面倒だし見にくいなーと思ったので、
Firestoreのdocument取得をみやすく書けるようにしてみました。

方針: 簡単なメソッドチェーンで、自分で定義したモデルクラスに落とし込まれた状態のデータを取得できるようにする

具体的にいうと、このコードを

// 単一データの取得
db.collection("cities").document("SF").getDocument { (document, error) in
    if let document = document, document.exists {
        let data = document.data()
        let city = self.parse(data) // 何らかの方法でparseする
        self.hoge(city) // 取得したデータで何かする
    } else {
        print("Document does not exist")
    }
}

// 複数データの取得
db.collection("cities").getDocuments() { (querySnapshot, err) in
        if let err = err {
            print("Error")
        } else {
            let cities = for document in querySnapshot!.documents {
                let data = document.data()
                return self.parse(data) 
            }
            self.hoge(cities)
        }
}

こういう風に書けるようにします。

db.collection("cities").document("SF").fetch(as: City.self) { city in
  if let city = city {
     self.hoge(city)
  } else {
     print("error")
  }
}


// 複数データの取得
db.collection("cities").fetch(as: City.self) { cities in
  if let cities = cities {
     self.hoge(cities)
  } else {
     print("Error")
  }
}

どうやって?

  1. Codableに適合させたカスタムオブジェクトを作成する。
  2. Firestoreに実装されているクラスを拡張する。

やりかた

1. Codableに適合させたカスタムオブジェクトを作成する。

Codableについては、この記事がとても分かりやすいです(これより良く書けないし、丸投げします :santa: )
Firestoreに登録するドキュメントと、モデルクラスを対応させることで、
data(as:)でFirestoreのデータ構造からモデルオブジェクトに自動的に変換させられます。

// 単一データ取得。documentからデータのparseのところがちょっとわかりやすくなる。
db.collection("cities").document("SF").getDocument() { document, err in
  if let document = document, document.exists {
     do {
        let city = try document.data(as: City.self) // こういう風に書けるようになる
        self.hoge(city)
     } catch let error {
        print("error")
     }
  } else {
     print("error")
  }
}

// 複数データ取得。
db.collection("cities").getDocuments() { document, err in
  if let err = err 
    print("Error")
  } else {
    let cities = querySnapshot!.documents.flatMap { document in
      do {
        return try document.data(as: City.self) // これもこう書けるようになる。
      } catch let error {
        print("error")
      }
    }
    self.hoge(cities)
  }
}

data(as: City.self)を用いることで、コードが直感的に理解し安くなりました。
ここで気になるのは、結局毎回documentをオブジェクトに落とし混む処理を記述する必要があることです。
getDocumentgetDocumentsで返されるものがすでに処理済みのものだったらもっと楽に書けるのに。。。(複数取得の方、長いし) :cry:

2. Firestoreに実装されているクラスを拡張する。

なので、DocumentReferenceQueryを拡張して、いい感じに描けるようにしてみます。
それぞれ、#getDocument(as:)#getDocuments(as:)をラップしただけの関数です。

fetch(as:)を以下のように実装しました。コード!ドーン!!!

import Foundation
import FirebaseFirestore

extension DocumentReference {
    func fetch<T : Decodable> (as type: T.Type, _ completion: @escaping (T?) -> Void) {
        getDocument { (snapshot, error) in
            if let snapshot = snapshot, snapshot.exists {
                do {
                    let obj = try snapshot.data(as: type)
                    completion(obj)
                } catch let error {
                    print("[Error] #data(as:) throw error: \(error)")
                }
            } else if let error = error {
                print("[Error] #getDocument return error response: \(error)")
                completion(nil)
            } else { // !snapshot.existsのとき
                completion(nil)
            }
        }
    }
}

extension Query {
    func fetch<T : Decodable> (as type: T.Type, _ completion: @escaping ([T]?) -> Void) {
        getDocuments() { (snapshot, error) in
            if let error = error {
                print("[Error] #getDocument return error response: \(error)")
                completion(nil)
            } else {
                let docs = snapshot!.documents
                let objs:[T] = docs.compactMap { doc in
                    do {
                        return try doc.data(as: type)
                    } catch let error {
                        print("[Error] #data(as:) throw error: \(error)")
                        return nil
                    }
                }
                completion(objs)
            }
        }
    }
}

シンプルなメソッドチェーンでカスタムオブジェクトを返すことで、
以下のようにみやすく書くことができるようになりました!!! :santa: :santa: :santa: :santa:

// (再掲)
// 単一データの取得
db.collection("cities").document("SF").fetch(as: City.self) { city in
  if let city = city {
     self.hoge(city)
  } else {
     print("error") // 
  }
}

// 複数データの取得。
db.collection("cities").fetch(as: City.self) { cities in
  if let cities = cities {
     self.hoge(cities)
  } else {
     responseError()
  }
}

注意点

注意点として、自分の紹介したコードでは、ラッパーメソッドfetch(as:)内でエラーを食べてしまっています。
自分はエラー種類によるロジック分岐が不要そうだったのでこのようにしていますが、必要な場合はerror戻すように修正いただければ... :bow:

おわり!!

趣味で触っているFirestore:fire: と趣味で触っているSwiftで記事を書きました...
他者に指摘されない環境で学んだため、変な記述があったかもしれません...
万が一、「その書き方おかしいよ、いかんだろ」とかあったら優しく教えてください :santa: :santa: :santa: :santa: :santa:

ありがとうございました!!!

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