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

基于Core Data FetchedResults的视图实时更新问题求助

问题描述

父视图通过Core Data读取数据生成子节点列表,期望数据更新时对应行视图实时刷新,但遇到以下问题:

  • 首次尝试用FetchedResults构建列表并传递@ObservedObject给行视图,仅切换标签页时才更新,无法实时生效。
  • 第二次通过NotificationCenter监听NSManagedObjectContextDidSave,配合@State的refreshID刷新列表,虽能触发重渲染,但.onReceive触发过于频繁,且外设推送消息时会意外关闭用于更新的Sheet视图。

相关代码

NodeList

struct NodeList: View {
    @Environment(\.managedObjectContext) var context
    @EnvironmentObject var bleManager: BLEManager
    @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "user.vip", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false)], animation: .default)

    var nodes: FetchedResults<RadioInfoEntity>

    private var didSave =  NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
    @State private var refreshID = UUID()
    @State private var path: NavigationPath = NavigationPath()

     var body: some View {
       NavigationStack (path: $path) {
        List(nodes, id: \.self) { node in
         NodeListItem(node: node,
                     connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num)
        .onReceive(self.didSave) { _ in
                        self.refreshID = UUID()
                        print("generated a new UUID")
                        }
        }.id(refreshID)
        .onAppear {
          if self.bleManager.context == nil {
          self.bleManager.context = context
            }
        }
      }
     }
   }

NodeListItem & SheetView

struct NodeListItem: View {

  @Environment(\.managedObjectContext) var context
  @EnvironmentObject var bleManager: BLEManager
  @ObservedObject var node: RadioInfoEntity
  @State private var name: String = ""
  @State private var showingSheetView = false

  var body: some View { 
   LazyVStack(alignment: .leading) {
     Text(name)
     let status = getStatus(status: name)
     Text(status)
     Button("Update") {
        showingSheetView.toggle()
              } .sheet(isPresented: $showingSheetView) {
                SheetView(user: node)
            }
     }.onAppear(perform: {
        if let tempName = node.user?.name{
          self.name = tempName
       }
      })
     }
   }


struct SheetView: View {
  @Environment(\.dismiss) var SheetViewDismiss
  @ObservedObject var user: RadioInfoEntity
  @State private var name: String = ""
  @State private var customName: String = ""
  @State private var longName: String = ""
  var connectedUser: User?

  var body: some View {
     HStack{
      TextField("name", text: $customName)
     }
     .onAppear(perform: {
         self.longName = user.user?.name ?? ""
       })
      Button("Update") {
        self.user.objectWillChange.send()
        var u = User()
        u.name = getUpdatedName(name: name)
        bleManager.saveUser(config: u, fromUser: connectedUser, toUser: user.user!)
        SheetViewDismiss()
        }
     }
   }
 }

BLEManager

class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
   var context: NSManagedObjectContext?
   let FROMRADIO_UUID = CBUUID(string: "YOUR_UUID_HERE")
   var decodedInfo: DecodedInfo! // 需补充解码逻辑

   func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    switch characteristic.uuid {
    case FROMRADIO_UUID:
     switch decodedInfo.packet.decoded.portnum {
      case .nodeinfoApp:
      upsertNodeInfoPacket(packet: decodedInfo.packet, context: context!)
      }
     }
  }
 }

Core Data 更新逻辑

func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) {
   let fetchNodeInfoAppRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "RadioInfoEntity")
        fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
   do {
    let fetchedRadio = try context.fetch(fetchNodeInfoAppRequest) as? [RadioInfoEntity] ?? []
     fetchedRadio[0].id = Int64(packet.from)
     fetchedRadio[0].num = Int64(packet.from)
     fetchedRadio[0].lastHeard = Date()
     if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
      if nodeInfoMessage.hasUser {
       fetchedRadio[0].user!.userId = nodeInfoMessage.user.id
       fetchedRadio[0].user!.num = Int64(nodeInfoMessage.num)
       fetchedRadio[0].user!.name = nodeInfoMessage.user.name
      }
     }
     do {
       try context.save()
       print("💾 Updated NodeInfo from Node Info App Packet For: \(fetchedRadio[0].num)")
       } catch {
            context.rollback()
            let nsError = error as NSError
           print("💥 Error Saving RadioInfoEntity from NODEINFO_APP \(nsError)")
        }
        } catch {
          print("💥 Error Fetching RadioInfoEntity for NODEINFO_APP")
        }
}

解决方案

1. 修复FetchedResults实时更新失效问题

行视图未实时刷新是因为将node.user?.name存入@State变量,切断了与@ObservedObject的绑定。直接在视图中使用实体属性,让@ObservedObject自动监听变化:

struct NodeListItem: View {
  @Environment(\.managedObjectContext) var context
  @EnvironmentObject var bleManager: BLEManager
  @ObservedObject var node: RadioInfoEntity
  @State private var showingSheetView = false

  var body: some View { 
   LazyVStack(alignment: .leading) {
     Text(node.user?.name ?? "Unknown")
     let status = getStatus(status: node.user?.name ?? "")
     Text(status)
     Button("Update") {
        showingSheetView.toggle()
     }
     .sheet(isPresented: $showingSheetView) {
        SheetView(user: node)
     }
   }
  }
}

2. 移除冗余的NotificationCenter监听

FetchedResults原生支持Core Data变化监听,删除NodeList中didSave发布者、refreshID及相关代码,恢复原生更新能力:

struct NodeList: View {
    @Environment(\.managedObjectContext) var context
    @EnvironmentObject var bleManager: BLEManager
    @FetchRequest( 
        sortDescriptors: [
            NSSortDescriptor(key: "user.vip", ascending: false), 
            NSSortDescriptor(key: "lastHeard", ascending: false)
        ], 
        animation: .default
    )
    var nodes: FetchedResults<RadioInfoEntity>
    @State private var path: NavigationPath = NavigationPath()

     var body: some View {
       NavigationStack (path: $path) {
        List(nodes, id: \.self) { node in
         NodeListItem(
             node: node,
             connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num
         )
        }
        .onAppear {
          if bleManager.context == nil {
              bleManager.context = context
          }
        }
      }
     }
   }

3. 修复Sheet意外关闭问题

Sheet关闭是因为refreshID导致整个List重渲染,行视图被重建后showingSheetView状态重置。移除refreshID后,List仅更新变化的行,Sheet状态不受影响。

4. 优化Core Data更新逻辑

增加空值判断避免崩溃,确保关联对象更新时上下文正确:

func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) {
   let fetchNodeInfoAppRequest: NSFetchRequest<RadioInfoEntity> = RadioInfoEntity.fetchRequest()
   fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
   do {
    guard let radio = try context.fetch(fetchNodeInfoAppRequest).first else {
        print("⚠️ No RadioInfoEntity found for num: \(Int64(packet.from))")
        return
    }
    radio.id = Int64(packet.from)
    radio.num = Int64(packet.from)
    radio.lastHeard = Date()
    
    if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload), nodeInfoMessage.hasUser {
        guard let user = radio.user else {
            print("⚠️ RadioInfoEntity has no associated User")
            return
        }
        user.userId = nodeInfoMessage.user.id
        user.num = Int64(nodeInfoMessage.num)
        user.name = nodeInfoMessage.user.name
    }
    
    try context.save()
    print("💾 Updated NodeInfo from Node Info App Packet For: \(radio.num)")
   } catch let nsError as NSError {
        context.rollback()
        print("💥 Error Saving RadioInfoEntity from NODEINFO_APP \(nsError)")
   }
}

5. 确保Core Data实体配置正确

  • 在Xcode模型编辑器中,为RadioInfoEntity和User的所有属性勾选Dynamic,确保属性变化能触发objectWillChange。
  • 配置双向关联关系:User实体添加反向关联指向RadioInfoEntity,确保更新User时,RadioInfoEntity能收到变化通知。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 18:45:12