アドベントカレンダー1日目! 一番いい場所いただきました!
内容
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")
}
}
どうやって?
-
Codable
に適合させたカスタムオブジェクトを作成する。 - Firestoreに実装されているクラスを
拡張
する。
やりかた
1. Codableに適合させたカスタムオブジェクトを作成する。
Codableについては、この記事がとても分かりやすいです(これより良く書けないし、丸投げします )
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をオブジェクトに落とし混む処理を記述する必要があることです。
getDocument
やgetDocuments
で返されるものがすでに処理済みのものだったらもっと楽に書けるのに。。。(複数取得の方、長いし)
2. Firestoreに実装されているクラスを拡張する。
なので、DocumentReference
とQuery
を拡張して、いい感じに描けるようにしてみます。
それぞれ、#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)
}
}
}
}
シンプルなメソッドチェーンでカスタムオブジェクトを返すことで、
以下のようにみやすく書くことができるようになりました!!!
// (再掲)
// 単一データの取得
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戻すように修正いただければ...
おわり!!
趣味で触っているFirestore と趣味で触っているSwiftで記事を書きました...
他者に指摘されない環境で学んだため、変な記述があったかもしれません...
万が一、「その書き方おかしいよ、いかんだろ」とかあったら優しく教えてください
ありがとうございました!!!