You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI视图在Firestore对象更新后未自动刷新的问题求助

问题:SwiftUI + Firestore 列表无法自动同步更新

在SwiftUI项目中用Firestore存储条目列表,编辑页面关闭时数据库已成功更新,但列表视图不会自动刷新,只有通过分段选择器切换到其他视图再返回,才会显示最新数据。试过以下方法都无效:

  • 编辑页面消失时调用fetchData
  • 把viewModel作为环境对象传入,编辑完成按钮点击时调用fetchData
  • 编辑页面关闭时手动切换视图再切回

解决方案

核心问题

  1. Firestore快照监听器的回调在后台线程执行,直接更新@Published属性会导致SwiftUI无法正确检测到数据变化,进而不触发视图刷新
  2. 每次调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 08:05:57