はじめに
@FirestoreQueryとは、以下のようなやつです。
@FirestoreQuery(collectionPath: "users") var users: [User]
こうやって書くだけで自動でFirestoreからusersコレクションのデータを撮ってきてくれる!しかも勝手にUser型に変換してくれる!!嬉しい!!
と、とても便利なはずの@FirestoreQuery君なのですが、あまり使われてる場面や記事を見かけません。なぜ使いにくいのか、そして使いやすくなる方法を考えたので、ぜひ読んでみてください。
@FirestoreQueryでできること
前述の通り、書くだけでコレクションのデータ取得・変化の監視をすることができます。
このとき、UserはCodableに準拠したstructである必要があります。
import FirebaseFirestore
struct User: Identifiable, Codable {
@DocumentID var id: String?
var name: String
var email: String
}
ここで、@DocumentIDをつけると勝手にドキュメントのIDを取得してくれます!(RealtimeDatabaseの頃は手動だったから大変だった…良い時代になりました。)
余談:そもそもFirestoreとCodableの相性がすこぶる良いです。
@FirestoreQueryの残念なこと
Pathに変数を含められない
これが一番残念な点。例えばNavigationLinkで画面遷移するとします。
ここでUserの各々のドキュメントの中にscoresコレクションがあったとします。
このscoresコレクションへのパスusers/{document_id}/scoresを**うまく定義できない。つまり、以下の書き方がエラーになります。
@FirestoreQuery(collectionPath: "users/\(userID)") var users: [User]
// Cannot use instance member 'path' within property initializer; property initializers run before 'self' is available
ドキュメント内のコレクションはうまくとってこれない
これは普通にFirestoreの悪いところですが、上のようにscoresを配置しても、[Score]として自動でDecodeしてくれません。
無理やり辞書型でドキュメント内に記述してもいいですが、structとの対応などを考えるとあまり良くないですよね。
また、usersを監視しているだけではscoresを取得できないなどもあり、大変です。
@FirestoreQueryをうまく使おう
さて、本題として、重要なのはどうやってcollectionPathに変数を含めるかです。これがどうやっても記事が出てこなかった…
結論から言うと以下のように変更することができます。
.onAppear {
$users.path = path
}
usersをBindingとして扱ってあげる必要があり、しかもcollectionPathではなく単にpathと指定するようです。
例
上で作った、usersの中にscoresがあるものを例にすれば、
import SwiftUI
import FirebaseFirestore
struct User: Identifiable, Codable {
@DocumentID var id: String?
var name: String
var email: String
}
struct ContentView: View {
@FirestoreQuery(collectionPath: "users") var users: [User]
var body: some View {
NavigationStack {
List(users) { user in
NavigationLink {
NextView(path: $users.path + "/" + user.id! + "/scores")
} label: {
VStack(alignment: .leading) {
Text(user.name)
.font(.headline)
Text(user.email)
.font(.subheadline)
}
}
}
}
}
}
import SwiftUI
import FirebaseFirestore
struct Score: Identifiable, Codable {
@DocumentID var id: String?
var score: Int
}
struct NextView: View {
@FirestoreQuery(collectionPath: "empty") var scores: [Score]
var path: String
var body: some View {
List(scores) { score in
Text(String(score.score))
}
.onAppear {
$scores.path = path
}
}
}
NextViewで、最初はcollectionPathを"empty"とするのなどはアンチパターンかも知れませんが、とにかくこうすればうまくいく。というのが言いたかったことです。
まとめ
手軽にFirestoreの変更を監視できる上、getDocumentsなどを全く書かなくてもなんとかなるのは本当に手軽なので、なにかしら活用できればいいなと思ってこの記事を書きました。
ともあれ、.onAppearで変更するしかなかったり、アーキテクチャのことを考えると、Readは@FirestoreQueryでView内で行い、Create/Update/DeleteはFirestoreのレポジトリから行う、など分離するのが良くなかったりといった意見もあるかも知れません。この辺はベストプラクティスを探っていきたいなと思っています。
