SwiftUI中@FirestoreQuery权限问题及数据实时更新咨询
SwiftUI中Firebase实时监听失效的解决办法
初始场景与权限问题
最初使用@FirestoreQuery从Firebase获取项目列表,安全规则开放时(允许所有登录用户读写)一切正常:
match /databases/{database}/documents { match /projects/{projectName=**} { allow read, write: if request.auth != null; } }
但修改为仅允许文档所有者读写后:
match /databases/{database}/documents { match /projects/{projectName} { allow read, update, delete: if request.auth != null && resource.data.owner == request.auth.uid; allow create: if request.auth != null; } }
出现权限错误:[FirebaseFirestore][I-FST000001] Listen for query at projects failed: Missing or insufficient permissions。
需要给查询添加owner等于当前用户ID的过滤条件,但直接在@FirestoreQuery的collectionPath中用字符串插值无效,因为它仅接受静态路径字符串。
相关代码
Project Model
import Foundation import Firebase import FirebaseFirestoreSwift struct Project: Identifiable, Codable, Hashable { @DocumentID var id: String? var projectName = "" var createdOn = Date() var createdBy = "" var projectColor = "" var owner = "" var dictionary: [String: Any] { return ["projectName": projectName, "createdOn": Timestamp(date: Date()), "createdBy": Auth.auth().currentUser?.email ?? "", "projectColor": projectColor, "owner": Auth.auth().currentUser?.uid ?? ""] } }
初始视图代码
struct MainView: View { @FirestoreQuery(collectionPath: "projects", predicates: [.orderBy("createdOn", true)] ) var projects: [Project] @Environment(\.dismiss) private var dismiss @EnvironmentObject var mainViewVM: MainViewModel @State private var settingsViewSheetIsPresented = false @State private var createProjectViewSheetIsPresented = false @State private var searchTerm = "" var body: some View { VStack(alignment: .leading, spacing: 0) { NavigationStack { List { ForEach(projects) { project in HStack { Rectangle() .fill(Color("\(project.projectColor)")) .frame(width:10) VStack(alignment: .leading, spacing: 5) { HStack { NavigationLink { ProjectView(project: project) .environmentObject(TeamViewModel()) } label: { Text(project.projectName) .font(.title2.bold()) .lineLimit(1) .foregroundStyle(Color("\(project.projectColor)")) } } HStack { Text("Created \(project.createdOn.formatted(date:.abbreviated, time: .shortened)), by \(project.createdBy)") .foregroundStyle(.secondary) .font(.caption2) .lineLimit(1) .minimumScaleFactor(0.5) } } } } } // ... 其余视图代码 } } } }
尝试的解决方案与新问题
改用ViewModel手动实现查询,添加了owner和accessList的过滤条件,配合更新后的安全规则可以正常获取数据:
更新后的安全规则
match /projects/{projectName=**} { allow read, update, delete: if !('owner' in resource.data) || request.auth.uid == resource.data.owner; allow read, update, delete: if request.auth.token.email_verified in resource.data.accessList || request.auth.uid == resource.data.owner; allow create: if request.auth.uid != null; }
ViewModel代码
class MainViewModel: ObservableObject { @Published var projects: [Project] = [] private var db = Firestore.firestore() init() { loadProjects() } func loadProjects() { guard let userId = Auth.auth().currentUser?.uid, let userEmail = Auth.auth().currentUser?.email else { return } db.collection("projects").whereFilter(Filter.orFilter([ Filter.whereField("owner", isEqualTo: userId), Filter.whereField("accessList", arrayContains: userEmail) ])) .order(by: "createdOn", descending: true) .addSnapshotListener { (querySnapshot, error) in if let querySnapshot = querySnapshot { self.projects = querySnapshot.documents.compactMap { document -> Project? in try? document.data(as: Project.self) } } } } }
但出现新问题:文档更新或添加时无法触发实时响应。
解决实时监听失效的方案
1. 保留监听引用,避免被自动释放
Firestore的addSnapshotListener返回ListenerRegistration对象,如果不保留该引用,系统会自动释放监听,导致无法持续接收更新。修改ViewModel,添加监听引用并在销毁时移除。
2. 切换到主队列更新UI数据
Firestore的快照回调在后台线程执行,直接更新@Published属性可能无法触发UI刷新,需要手动切换到主队列。
修改后的ViewModel代码
class MainViewModel: ObservableObject { @Published var projects: [Project] = [] private var db = Firestore.firestore() private var listenerRegistration: ListenerRegistration? // 保存监听引用 init() { loadProjects() } func loadProjects() { // 先移除旧监听,避免重复监听 listenerRegistration?.remove() guard let userId = Auth.auth().currentUser?.uid, let userEmail = Auth.auth().currentUser?.email else { return } // 保存监听引用 listenerRegistration = db.collection("projects") .whereFilter(Filter.orFilter([ Filter.whereField("owner", isEqualTo: userId), Filter.whereField("accessList", arrayContains: userEmail) ])) .order(by: "createdOn", descending: true) .addSnapshotListener { [weak self] (querySnapshot, error) in guard let self = self else { return } if let error = error { print("监听失败:\(error.localizedDescription)") return } guard let querySnapshot = querySnapshot else { return } // 切换到主队列更新UI数据 DispatchQueue.main.async { self.projects = querySnapshot.documents.compactMap { document -> Project? in try? document.data(as: Project.self) } } } } // 销毁ViewModel时移除监听 deinit { listenerRegistration?.remove() } }
3. 验证查询条件匹配
确保新增/修改的文档满足以下任一条件,否则不会被查询到,也就不会触发监听更新:
owner字段等于当前用户的UIDaccessList数组包含当前用户的已验证邮箱
补充:@FirestoreQuery的正确用法(可选)
如果想回到@FirestoreQuery,可以通过初始化时传入动态predicates实现过滤:
struct MainView: View { @State private var userId: String? = Auth.auth().currentUser?.uid @FirestoreQuery var projects: [Project] // 初始化时设置predicates init() { let predicates: [FirestoreQueryPredicate] = [ .whereField("owner", isEqualTo: userId ?? ""), .orderBy("createdOn", descending: true) ] _projects = FirestoreQuery(collectionPath: "projects", predicates: predicates) } // 监听用户ID变化,重新初始化查询 var body: some View { // ... 视图内容 .onChange(of: userId) { newUserId in let predicates: [FirestoreQueryPredicate] = [ .whereField("owner", isEqualTo: newUserId ?? ""), .orderBy("createdOn", descending: true) ] _projects = FirestoreQuery(collectionPath: "projects", predicates: predicates) } } }
内容的提问来源于stack exchange,提问作者kuba
相关产品推荐
相关产品推荐

