はじめに
FirestoreではCodable
に準拠したstructを用いることで簡単にデータの読み書きを行うことができます。また、Firestore上での変更を監視することも可能です。
しかし、structの配列を持つ場合、簡単に監視をすることができません。
以前ここですごく詰まったので、記事にまとめます。
現状の構造
User
がScore
の配列を持つものとします。
struct User: Codable {
@DocumentID var id: String?
var name: String
var email: String
var scores: [Score]
}
struct Score: Codable {
@DocumentID var id: String?
var score: Int
}
また、監視のためのaddSnapshotListener
をこのように書いているとします。
listener = db.collection("users").addSnapshotListener { snapshot, error in
self.users = snapshot!.documents.compactMap { document in
try? document.data(as: User.self)
}
}
Firestore上の構成はこのようになっているとします。
しかし、このように取得すると取得ができず、エラーになってしまいます。
原因
documentの中にあるcollectionは、Codableの配列として扱われません!
もしscores
もusers
の監視で取得したければ、以下のような構成にしなければなりません。
しかしこれだと、documentの中にcollectionが作れる良さが薄れますね…
改善案
ということで、usersのaddSnapshotListener
の時に、各scoresにもaddSnapshotListener
をしましょう。
struct User: Codable, Identifiable {
@DocumentID var id: String?
var name: String
var email: String
var scores: [Score] = [] //こうすることでinit時にエラーが起こらない
}
struct Score: Codable {
@DocumentID var id: String?
var score: Int
}
class UsersViewModel: ObservableObject {
@Published var users: [User] = []
private var db = Firestore.firestore()
private var userListeners: [ListenerRegistration] = []
private var mainListener: ListenerRegistration?
init() {
fetchUsers()
}
func fetchUsers() {
mainListener = db.collection("users").addSnapshotListener { snapshot, error in
if let error = error {
print("Error fetching users: \(error.localizedDescription)")
return
}
guard let documents = snapshot?.documents else {
print("No documents found")
return
}
self.users = documents.compactMap { document in
try? document.data(as: User.self)
}
self.removeUserListeners()
for user in self.users {
if let userID = user.id {
self.fetchScores(for: userID)
}
}
}
}
func fetchScores(for userID: String) {
let listener = db.collection("users").document(userID).collection("scores").addSnapshotListener { snapshot, error in
if let error = error {
print("Error fetching scores for user \(userID): \(error.localizedDescription)")
return
}
guard let documents = snapshot?.documents else {
print("No scores found for user \(userID)")
return
}
let scores = documents.compactMap { document in
try? document.data(as: Score.self)
}
DispatchQueue.main.async {
if let index = self.users.firstIndex(where: { $0.id == userID }) {
self.users[index].scores = scores
}
}
}
userListeners.append(listener)
}
private func removeUserListeners() {
for listener in userListeners {
listener.remove()
}
userListeners.removeAll()
}
deinit {
mainListener?.remove()
removeUserListeners()
}
}