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

Firestore监听器触发后UI无法更新问题求助

问题:Firestore文档更新时二级视图UI无法同步更新

我需要在Firestore文档变化时更新UI,控制台已确认每次文档变更都会触发监听器。监听器和readyOrders均使用@Published修饰。

监听器代码

func getReadyOrders() {
    referance
        .collection(path)
        .document(email)
        .collection("CompletedOrders")
        .whereField("placedBy", isEqualTo: user)
        .addSnapshotListener { orderSnapshot, error in
            DispatchQueue.main.async {
                guard let snapshot = orderSnapshot?.documents else{
                    print("There is no active orders")
                    return
                }
                
                self.readyOrders = snapshot.map{ activeSnapshot -> ActiveOrder in
                    let data = activeSnapshot.data()
                    
                    var collectedItems = [MenuItem]()
                    var collectedDrinks = [DrinkItem]()
                    
                    let id = activeSnapshot.documentID
                    let placed = data["placedBy"] as? String ?? ""
                    let inZone = data["inZone"] as? String ?? ""
                    let forTable = data["forTable"] as? String ?? ""
                    let orderItems = data["orderItems"] as? [String]
                    let orderDrinks = data["orderDrinks"] as? [String]
                    let orderItemsReady = data["orderItemsReady"] as? Bool ?? false
                    let orderDrinksReady = data["orderDrinksReady"] as? Bool ?? false
                    let totalAmount = data["totalAmount"] as? Double ?? 0.00
                    
                    orderItems?.forEach({ item in
                        let parts = item.components(separatedBy: "/")
                        collectedItems.append(MenuItem(itemName: parts[0], price: Double(parts[1])))
                    })
                    
                    orderDrinks?.forEach({ drink in
                        let itemPart = drink.components(separatedBy: "/")
                        collectedDrinks.append(DrinkItem(drinkName: itemPart[0], price: Double(itemPart[1])))
                    })
                    
                    return ActiveOrder(id: id,
                                       placedBy: placed,
                                       inZone: inZone,
                                       forTable: forTable,
                                       orderItems: collectedItems,
                                       orderDrinks: collectedDrinks,
                                       orderItemsReady: orderItemsReady,
                                       orderDrinksReady: orderDrinksReady,
                                       totalAmount: totalAmount)
                }
            }
        }
}

一级视图(可正常更新)

一级视图展示所有文档,新增或删除文档时UI能正常更新:

Section {
    ForEach(handler.readyOrders, id: \.id){ readyOrder in
        NavigationLink{
            OrderComplete(handler: handler, order: readyOrder, currency: currency)
        } label: {
            HStack{
                Text(readyOrder.inZone!)
                Text("- \(readyOrder.forTable!)")
            }
        }
    }
} header: {
    Text("Order's ready:")
}

二级视图(无法更新)

二级视图接收ForEach传递的readyOrder,通过内部ForEach展示订单内的数组,但文档内容变更时UI无法更新:

ForEach(order.orderItems!, id:\.id){ item in
    HStack{
        Text(item.itemName!)
        Spacer()
        Text(currency.format(item.price!))
            .foregroundColor(.teal)
        Image(systemName: "arrow.left")
            .foregroundColor(.red)
    }
}
.onDelete(perform: deleteItem)

最小复现代码

模型代码

struct Order: Identifiable{
    var id = UUID().uuidString
    var placedBy: String?
    var inZone: String?
    var forTable: String?
    var orderItems: [MenuItem]?
    var orderDrinks: [DrinkItem]?
    var orderItemsReady: Bool?
    var orderDrinksReady: Bool?
    var totalAmount: Double?
}

ViewModel代码

class ViewModel: ObservableObject{
    @Published var orders = [Order]()

    private var referance = Firestore.firestore()

    func getReadyOrders() {
        referance
            .collection(path)
            .document(email)
            .collection("CompletedOrders")
            .whereField("placedBy", isEqualTo: user)
            .addSnapshotListener { orderSnapshot, error in
                DispatchQueue.main.async {
                    guard let snapshot = orderSnapshot?.documents else{
                        print("There is no active orders")
                        return
                    }
                    
                    self.readyOrders = snapshot.map{ activeSnapshot -> ActiveOrder in
                        let data = activeSnapshot.data()
                        
                        var collectedItems = [MenuItem]()
                        var collectedDrinks = [DrinkItem]()
                        
                        let id = activeSnapshot.documentID
                        let placed = data["placedBy"] as? String ?? ""
                        let inZone = data["inZone"] as? String ?? ""
                        let forTable = data["forTable"] as? String ?? ""
                        let orderItems = data["orderItems"] as? [String]
                        let orderDrinks = data["orderDrinks"] as? [String]
                        let orderItemsReady = data["orderItemsReady"] as? Bool ?? false
                        let orderDrinksReady = data["orderDrinksReady"] as? Bool ?? false
                        let totalAmount = data["totalAmount"] as? Double ?? 0.00
                        
                        orderItems?.forEach({ item in
                            let parts = item.components(separatedBy: "/")
                            collectedItems.append(MenuItem(itemName: parts[0], price: Double(parts[1])))
                        })
                        
                        orderDrinks?.forEach({ drink in
                            let itemPart = drink.components(separatedBy: "/")
                            collectedDrinks.append(DrinkItem(drinkName: itemPart[0], price: Double(itemPart[1])))
                        })
                        
                        return ActiveOrder(id: id,
                                           placedBy: placed,
                                           inZone: inZone,
                                           forTable: forTable,
                                           orderItems: collectedItems,
                                           orderDrinks: collectedDrinks,
                                           orderItemsReady: orderItemsReady,
                                           orderDrinksReady: orderDrinksReady,
                                           totalAmount: totalAmount)
                    }
                }
            }
    }

    func delteItem(menuItem: MenuItem, from order: ActiveOrder){
      let item = menuItem.itemName! + "/" + String(menuItem.price!)
      
      let pathTo = referance.collection(path).document(email).collection("CompletedOrders").document(order.id)
          
          pathTo.getDocument { snapshot, error in
              if let document = snapshot, document.exists{
                  var items = document.data()!["orderItems"] as? [String] ?? []
                  let index = items.firstIndex(where: { $0 == item })
                  items.remove(at: index!)
                  
                  pathTo.updateData(["orderItems" : items]){ error in
                      if let _ = error{
                          print("Error deleting and updating order array")
                      }
                  }
              }
          }
    }
}

视图代码

struct View1: View{
    @ObservedObject var viewModel: ViewModel
    var body: some View{
        VStack{
            ForEach(viewModel.orders, id: \.id){ readyOrder in
                NavigationLink{
                    View2(viewModel: viewModel, order: readyOrder)
                } label: {
                    HStack{
                        Text(readyOrder.inZone!)
                        Text("- \(readyOrder.forTable!)")
                    }
                }
            }
        }
    }
}

struct View2: View{
    @ObservedObject var viewModel: ViewModel
    var order: Order

    func deleteItem(at offest: IndexSet){
        let index = offest[offest.startIndex]
        let deleteItem = order.orderItems![index]
        
        handler.delteItem(menuItem: deleteItem, from: order)
    }

    // 希望在此视图中展示文档更新后的内容(包括元素被移除的情况)
    var body: some View{
        VStack{
            ForEach(order.orderItems!, id:\.id){ item in
                HStack{
                    Text(item.itemName!)
                    Spacer()
                    Text(item.price!)
                        .foregroundColor(.teal)
                    Image(systemName: "arrow.left")
                        .foregroundColor(.red)
                }
            }
            .onDelete(perform: deleteItem)
        }
    }
}

解决方案

问题核心是二级视图持有了order值类型的旧副本,ViewModel的orders数组更新时,二级视图不会自动同步最新数据。

修复步骤

  1. 二级视图不再直接接收order参数,改为接收订单id
  2. 通过ViewModel的orders数组,根据id实时获取最新订单数据

修改后的View2代码:

struct View2: View{
    @ObservedObject var viewModel: ViewModel
    let orderId: String // 改为接收订单ID

    // 通过ID获取最新订单
    private var currentOrder: Order? {
        viewModel.orders.first(where: { $0.id == orderId })
    }

    func deleteItem(at offest: IndexSet){
        guard let order = currentOrder, let deleteItem = order.orderItems?[offest[offest.startIndex]] else { return }
        viewModel.delteItem(menuItem: deleteItem, from: order)
    }

    var body: some View{
        VStack{
            if let order = currentOrder, let items = order.orderItems {
                ForEach(items, id:\.id){ item in
                    HStack{
                        Text(item.itemName!)
                        Spacer()
                        Text("\(item.price!)")
                            .foregroundColor(.teal)
                        Image(systemName: "arrow.left")
                            .foregroundColor(.red)
                    }
                }
                .onDelete(perform: deleteItem)
            }
        }
    }
}

同时修改View1中的NavigationLink参数传递:

NavigationLink{
    View2(viewModel: viewModel, orderId: readyOrder.id)
} label: {
    HStack{
        Text(readyOrder.inZone!)
        Text("- \(readyOrder.forTable!)")
    }
}

额外优化建议

  • 模型中的可选值尽量改为非可选(根据业务场景),减少强制解包风险
  • MenuItem和DrinkItem需遵循Identifiable协议,确保id唯一性
  • 修正ViewModel中delteItem的拼写错误为deleteItem

内容的提问来源于stack exchange,提问作者Ivars

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 09:21:35