はじめに
@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のレポジトリから行う、など分離するのが良くなかったりといった意見もあるかも知れません。この辺はベストプラクティスを探っていきたいなと思っています。