SwiftUI视图在Firestore对象更新后未自动刷新的问题求助
问题:SwiftUI + Firestore 列表无法自动同步更新
在SwiftUI项目中用Firestore存储条目列表,编辑页面关闭时数据库已成功更新,但列表视图不会自动刷新,只有通过分段选择器切换到其他视图再返回,才会显示最新数据。试过以下方法都无效:
- 编辑页面消失时调用
fetchData - 把viewModel作为环境对象传入,编辑完成按钮点击时调用
fetchData - 编辑页面关闭时手动切换视图再切回
解决方案
核心问题
- Firestore快照监听器的回调在后台线程执行,直接更新
@Published属性会导致SwiftUI无法正确检测到数据变化,进而不触发视图刷新 - 每次调用
fetchData都会添加新的监听器,未移除旧监听器,造成重复监听、内存泄漏,甚至数据更新冲突
修复步骤
1. 修正ListVM的线程问题与监听器生命周期
修改ListVM.swift,将快照更新逻辑切换到主线程,同时保存监听器句柄,在ViewModel销毁时移除,避免重复监听:
import Foundation import FirebaseFirestore class ListVM: ObservableObject { @Published var showingNewItemView = false @Published var showingProfileView = false @Published var items = [Item]() @Published var toDoShowing: Bool = false private var userId: String private var listenerRegistration: ListenerRegistration? // 保存监听器句柄 init(userId: String) { self.userId = userId } // 新增:提供已排序的计算属性,避免在视图层直接排序 var sortedItems: [Item] { items.sorted { $0.dueDate < $1.dueDate } } func fetchData() { // 先移除旧监听器,避免重复监听 listenerRegistration?.remove() let db = Firestore.firestore() listenerRegistration = db.collection("users") .document(userId) .collection("items").addSnapshotListener{(querySnapshot, error) in guard let documents = querySnapshot?.documents else { print("No documents") return } // 切换到主线程更新@Published属性 DispatchQueue.main.async { self.items = documents.map{ (queryDocumentSnapshot) -> Item in let data = queryDocumentSnapshot.data() let id = data["id"] as? String ?? "" let title = data["title"] as? String ?? "" let period = data["period"] as? Double ?? 0.0 let dueDate = data["dueDate"] as? TimeInterval ?? 0.0 let createdDate = data["createdDate"] as? TimeInterval ?? 0.0 return Item(id: id, title: title, period: period, dueDate: dueDate, createdDate: createdDate) } } } } // 销毁时移除监听器,释放资源 deinit { listenerRegistration?.remove() } }
2. 优化ListView的ForEach使用
将视图层的排序逻辑转移到ViewModel的计算属性中,确保数据更新时视图能正确响应:
@ViewBuilder func habitView() -> some View { ForEach(viewModel.sortedItems) { item in NavigationLink(destination: ItemEditView(item: item) .navigationBarBackButtonHidden(true) .environmentObject(viewModel) ) { ListItemView(item: item) .frame(width: 370) } .tint(Colors.PRIMARY) } }
3. 简化Item的save方法
移除不必要的新实例创建,直接使用当前实例属性更新数据库:
public func save() { guard canSave else { print("Cant save") return } guard let uid = Auth.auth().currentUser?.uid else { print("Problem with uid") return } let db = Firestore.firestore() db.collection("users") .document(uid) .collection("items") .document(id) .setData([ "id": id, "title": title, "period": period, "dueDate": dueDate, "createdDate": createdDate ]) }
为什么之前的方法无效?
- 手动调用
fetchData会添加重复监听器,且后台线程更新数组时,SwiftUI无法检测到变化 - 环境对象传递正确,但数据更新未在主线程执行,视图无法同步刷新
完整相关代码
ListView.swift
import SwiftUI import FirebaseFirestoreSwift struct ListView: View { @StateObject var viewModel: ListVM init(userId: String) { self._viewModel = StateObject( wrappedValue: ListVM(userId: userId) ) } var body: some View { NavigationStack { ZStack { Colors.PRIMARY .ignoresSafeArea() VStack { Picker( "Show Register View", selection: $viewModel.toDoShowing, content: { Text("Habits").tag(false) Text("To-Dos").tag(true) } ) .pickerStyle(.segmented) .frame(width: 150) .onAppear(perform: { UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(Colors.SECONDARY) UISegmentedControl.appearance().backgroundColor = UIColor(Colors.TERTIARY) UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor(Colors.BLACK)], for: .selected) UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor(Colors.WHITE)], for: .normal) }) if !viewModel.toDoShowing { habitView() } else { Text("that worked") } Spacer() } VStack { Spacer() HStack { Button { viewModel.showingProfileView = true } label: { Image(systemName: "person.fill") .foregroundStyle(Colors.TERTIARY) .font(.system(size: 35)) .background(Circle() .fill(Colors.SECONDARY) .frame(width: 60, height: 60) ) } Spacer() .frame(width: 70) Button { viewModel.showingNewItemView = true } label: { Image(systemName: "plus") .foregroundStyle(Colors.TERTIARY) .font(.system(size: 35)) .background(Circle() .fill(Colors.SECONDARY) .frame(width: 60, height: 60) ) } } } } .sheet(isPresented: $viewModel.showingNewItemView, content: { NewItemView(showingNewItemView: $viewModel.showingNewItemView, habit: false) .navigationBarBackButtonHidden(true) }) .sheet(isPresented: $viewModel.showingProfileView, content: { ProfileView(showingProfileView: $viewModel.showingProfileView) .navigationBarBackButtonHidden(true) }) } .onAppear { viewModel.fetchData() } } @ViewBuilder func habitView() -> some View { ForEach(viewModel.sortedItems) { item in NavigationLink(destination: ItemEditView(item: item) .navigationBarBackButtonHidden(true) .environmentObject(viewModel) ) { ListItemView(item: item) .frame(width: 370) } .tint(Colors.PRIMARY) } } } #Preview { ListView(userId: "e9ntOkxDAEMvAkPAd01kWfr3IL83") }
Item.swift
import Foundation import Firebase struct Item: Codable, Identifiable { let id: String var title: String var period: Double var dueDate: TimeInterval let createdDate: TimeInterval mutating func setItemInfo(title: String, period: Double) { self.title = title self.period = period self.dueDate = Date.now.timeIntervalSince1970 self.updateDueDate() self.save() } mutating func updateDueDate() { var dueDate = Date(timeIntervalSince1970: self.dueDate) let calendar = Calendar.current var pd: Calendar.Component var amount: Int guard canUpdate else { return } if period == 0 { pd = .day amount = 1 } else if period == 1 { pd = .day amount = 7 } else if period == 2 { pd = .month amount = 1 } else { pd = .year amount = 1 } dueDate = calendar.date(byAdding: pd, value: amount, to: .now)! self.dueDate = dueDate.timeIntervalSince1970 self.save() } func daysBetweenTodayAndDueDate() -> Int { let calendar = Calendar.current return calendar.numberOfDaysBetween(Date.now, and: Date(timeIntervalSince1970: dueDate)) } func formatInterval() -> String { if period == 0 { return "Day" } else if period == 1 { return "Week" } else if period == 2 { return "Month" } else { return "Year" } } public func save() { guard canSave else { print("Cant save") return } guard let uid = Auth.auth().currentUser?.uid else { print("Problem with uid") return } let db = Firestore.firestore() db.collection("users") .document(uid) .collection("items") .document(id) .setData([ "id": id, "title": title, "period": period, "dueDate": dueDate, "createdDate": createdDate ]) } public func delete() { guard let uid = Auth.auth().currentUser?.uid else{ return } let db = Firestore.firestore() db.collection("users") .document(uid) .collection("items") .document(id) .delete() } var canSave: Bool { guard !title.trimmingCharacters(in: .whitespaces).isEmpty else { print("there") return false } guard self.dueDate >= Date().addingTimeInterval(-86400).timeIntervalSince1970 else { print("here") return false } return true } var canUpdate: Bool { let dueDate = Date(timeIntervalSince1970: self.dueDate) guard Date() >= dueDate else { return false } return true } }
ListVM.swift(已修正)
import Foundation import FirebaseFirestore class ListVM: ObservableObject { @Published var showingNewItemView = false @Published var showingProfileView = false @Published var items = [Item]() @Published var toDoShowing: Bool = false private var userId: String private var listenerRegistration: ListenerRegistration? init(userId: String) { self.userId = userId } var sortedItems: [Item] { items.sorted { $0.dueDate < $1.dueDate } } func fetchData() { listenerRegistration?.remove() let db = Firestore.firestore() listenerRegistration = db.collection("users") .document(userId) .collection("items").addSnapshotListener{(querySnapshot, error) in guard let documents = querySnapshot?.documents else { print("No documents") return } DispatchQueue.main.async { self.items = documents.map{ (queryDocumentSnapshot) -> Item in let data = queryDocumentSnapshot.data() let id = data["id"] as? String ?? "" let title = data["title"] as? String ?? "" let period = data["period"] as? Double ?? 0.0 let dueDate = data["dueDate"] as? TimeInterval ?? 0.0 let createdDate = data["createdDate"] as? TimeInterval ?? 0.0 return Item(id: id, title: title, period: period, dueDate: dueDate, createdDate: createdDate) } } } } deinit { listenerRegistration?.remove() } }
内容的提问来源于stack exchange,提问作者garrett
相关产品推荐
相关产品推荐

