当SwiftUI中的相关实体发生变化时,如何更新@FetchRequest?

Posted

技术标签:

【中文标题】当SwiftUI中的相关实体发生变化时,如何更新@FetchRequest?【英文标题】:How to update @FetchRequest, when a related Entity changes in SwiftUI? 【发布时间】:2020-02-26 19:09:47 【问题描述】:

在 SwiftUI View 中,我有一个基于 @FetchRequestList 显示 Primary 实体的数据和连接 Secondary 实体的通过关系。 View 及其 List 正确更新,当我添加一个新的 Primary 实体和一个新的相关辅助实体时。

问题是,当我在详细视图中更新连接的Secondary 项目时,数据库会更新,但更改不会反映在Primary 列表中。 显然,@FetchRequest 不会被另一个 View 中的更改触发。

之后,当我在主视图中添加新项目时,之前更改的项目最终会得到更新。

作为一种解决方法,我另外更新了详细视图中Primary 实体的属性,并且更改正确传播到Primary 视图。

我的问题是: 如何强制更新 SwiftUI Core Data 中所有相关的@FetchRequests? 特别是当我无法直接访问相关实体/@Fetchrequests?

import SwiftUI

extension Primary: Identifiable 

// Primary View

struct PrimaryListView: View 
    @Environment(\.managedObjectContext) var context

    @FetchRequest(
        entity: Primary.entity(),
        sortDescriptors: [NSSortDescriptor(key: "primaryName", ascending: true)]
    )
    var fetchedResults: FetchedResults<Primary>

    var body: some View 
        List 
            ForEach(fetchedResults)  primary in
                NavigationLink(destination: SecondaryView(primary: primary)) 
                VStack(alignment: .leading) 
                    Text("\(primary.primaryName ?? "nil")")
                    Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
                
                
            
        
        .navigationBarTitle("Primary List")
        .navigationBarItems(trailing:
            Button(action: self.addNewPrimary() ) 
                Image(systemName: "plus")
            
        )
    

    private func addNewPrimary() 
        let newPrimary = Primary(context: context)
        newPrimary.primaryName = "Primary created at \(Date())"
        let newSecondary = Secondary(context: context)
        newSecondary.secondaryName = "Secondary built at \(Date())"
        newPrimary.secondary = newSecondary
        try? context.save()
    


struct PrimaryListView_Previews: PreviewProvider 
    static var previews: some View 
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        return NavigationView 
            PrimaryListView().environment(\.managedObjectContext, context)
        
    


// Detail View

struct SecondaryView: View 
    @Environment(\.presentationMode) var presentationMode

    var primary: Primary

    @State private var newSecondaryName = ""

    var body: some View 
        VStack 
            TextField("Secondary name:", text: $newSecondaryName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
                .onAppear self.newSecondaryName = self.primary.secondary?.secondaryName ?? "no name"
            Button(action: self.saveChanges()) 
                Text("Save")
            
            .padding()
        
    

    private func saveChanges() 
        primary.secondary?.secondaryName = newSecondaryName

        // TODO: ❌ workaround to trigger update on primary @FetchRequest
        primary.managedObjectContext.refresh(primary, mergeChanges: true)
        // primary.primaryName = primary.primaryName

        try? primary.managedObjectContext?.save()
        presentationMode.wrappedValue.dismiss()
    

【问题讨论】:

没有帮助,抱歉。但我遇到了同样的问题。我的详细视图引用了选定的主要对象。它显示次要对象的列表。所有 CRUD 功能在 Core Data 中都能正常工作,但不会反映在 UI 中。很想了解这方面的更多信息。 您是否尝试过使用ObservableObject 我尝试在详细视图中使用 @ObservedObject var primary: Primary。但更改不会传播回主视图。 【参考方案1】:

另一种方法:使用 Publisher 和 List.id():

struct ContentView: View 
  /*
    @FetchRequest...
  */

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

  var body: some View 
      List 
        ...
      
      .id(refreshID)
      .onReceive(self.didSave)  _ in   //the listener
          self.refreshID = UUID()
          print("generated a new UUID")
          
  

每次在上下文中调用 NSManagedObjects 的 save() 时,它都会为 List 视图生成一个新的 UUID,并强制刷新 List 视图。

【讨论】:

【参考方案2】:

要解决此问题,您必须在 SecondaryView 中将 @ObservedObject 添加到 var primary: Primary 才能正常工作 ListPrimary 属于NSManagedObject 类,该类已经符合@ObservableObject 协议。这样Primary 实例的变化就会被观察到。

import SwiftUI

extension Primary: Identifiable 

// Primary View

struct PrimaryListView: View 
    @Environment(\.managedObjectContext) var context

    @FetchRequest(
        entity: Primary.entity(),
        sortDescriptors: [NSSortDescriptor(key: "primaryName", ascending: true)]
    )
    var fetchedResults: FetchedResults<Primary>

    var body: some View 
        List 
            ForEach(fetchedResults)  primary in
                NavigationLink(destination: SecondaryView(primary: primary)) 
                VStack(alignment: .leading) 
                    Text("\(primary.primaryName ?? "nil")")
                    Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
                
                
            
        
        .navigationBarTitle("Primary List")
        .navigationBarItems(trailing:
            Button(action: self.addNewPrimary() ) 
                Image(systemName: "plus")
            
        )
    

    private func addNewPrimary() 
        let newPrimary = Primary(context: context)
        newPrimary.primaryName = "Primary created at \(Date())"
        let newSecondary = Secondary(context: context)
        newSecondary.secondaryName = "Secondary built at \(Date())"
        newPrimary.secondary = newSecondary
        try? context.save()
    


struct PrimaryListView_Previews: PreviewProvider 
    static var previews: some View 
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        return NavigationView 
            PrimaryListView().environment(\.managedObjectContext, context)
        
    


// Detail View

struct SecondaryView: View 
    @Environment(\.presentationMode) var presentationMode

    @ObservedObject var primary: Primary

    @State private var newSecondaryName = ""

    var body: some View 
        VStack 
            TextField("Secondary name:", text: $newSecondaryName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
                .onAppear self.newSecondaryName = self.primary.secondary?.secondaryName ?? "no name"
            Button(action: self.saveChanges()) 
                Text("Save")
            
            .padding()
        
    

    private func saveChanges() 
        primary.secondary?.secondaryName = newSecondaryName

        try? primary.managedObjectContext?.save()
        presentationMode.wrappedValue.dismiss()
    

【讨论】:

【参考方案3】:

我也为此苦苦挣扎,找到了一个非常好的和干净的解决方案:

您必须将该行包装在单独的视图中,并在实体的该行视图中使用@ObservedObject。

这是我的代码:

酒单:

struct WineList: View 
    @FetchRequest(entity: Wine.entity(), sortDescriptors: [
        NSSortDescriptor(keyPath: \Wine.name, ascending: true)
        ]
    ) var wines: FetchedResults<Wine>

    var body: some View 
        List(wines, id: \.id)  wine in
            NavigationLink(destination: WineDetail(wine: wine)) 
                WineRow(wine: wine)
            
        
        .navigationBarTitle("Wines")
    

葡萄酒行:

struct WineRow: View 
    @ObservedObject var wine: Wine   // !! @ObserveObject is the key!!!

    var body: some View 
        HStack 
            Text(wine.name ?? "")
            Spacer()
        
    

【讨论】:

这对我来说非常有效。谢谢。事后看来,这也很明显,这很好,因为这意味着它符合 SwiftUI 的方式。谢谢。 这应该是公认的答案。它更简单,更符合 SwiftUI 理念。 多么优雅的解决方案! 关键是“@ObserveObject” 你震撼了!完美运行! OMFG!!如何?这怎么不是公认的答案?这很干净,简单,而且效果很好....谢谢,马克 【参考方案4】:

我尝试像这样触摸详细视图中的主要对象:

// TODO: ❌ workaround to trigger update on primary @FetchRequest

if let primary = secondary.primary 
   secondary.managedObjectContext?.refresh(primary, mergeChanges: true)

然后主列表将更新。但是细节视图必须知道父对象。这会起作用,但这可能不是 SwiftUI 或 Combine 的方式......

编辑:

基于上述解决方法,我使用全局 save(managedObject:) 函数修改了我的项目。这将触及所有相关实体,从而更新所有相关的@FetchRequest。

import SwiftUI
import CoreData

extension Primary: Identifiable 

// MARK: - Primary View

struct PrimaryListView: View 
    @Environment(\.managedObjectContext) var context

    @FetchRequest(
        sortDescriptors: [
            NSSortDescriptor(keyPath: \Primary.primaryName, ascending: true)]
    )
    var fetchedResults: FetchedResults<Primary>

    var body: some View 
        print("body PrimaryListView"); return
        List 
            ForEach(fetchedResults)  primary in
                NavigationLink(destination: SecondaryView(secondary: primary.secondary!)) 
                    VStack(alignment: .leading) 
                        Text("\(primary.primaryName ?? "nil")")
                        Text("\(primary.secondary?.secondaryName ?? "nil")")
                            .font(.footnote).foregroundColor(.secondary)
                    
                
            
        
        .navigationBarTitle("Primary List")
        .navigationBarItems(trailing:
            Button(action: self.addNewPrimary() ) 
                Image(systemName: "plus")
            
        )
    

    private func addNewPrimary() 
        let newPrimary = Primary(context: context)
        newPrimary.primaryName = "Primary created at \(Date())"
        let newSecondary = Secondary(context: context)
        newSecondary.secondaryName = "Secondary built at \(Date())"
        newPrimary.secondary = newSecondary
        try? context.save()
    


struct PrimaryListView_Previews: PreviewProvider 
    static var previews: some View 
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        return NavigationView 
            PrimaryListView().environment(\.managedObjectContext, context)
        
    


// MARK: - Detail View

struct SecondaryView: View 
    @Environment(\.presentationMode) var presentationMode

    var secondary: Secondary

    @State private var newSecondaryName = ""

    var body: some View 
        print("SecondaryView: \(secondary.secondaryName ?? "")"); return
        VStack 
            TextField("Secondary name:", text: $newSecondaryName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
                .onAppear self.newSecondaryName = self.secondary.secondaryName ?? "no name"
            Button(action: self.saveChanges()) 
                Text("Save")
            
            .padding()
        
    

    private func saveChanges() 
        secondary.secondaryName = newSecondaryName

        // save Secondary and touch Primary
        (UIApplication.shared.delegate as! AppDelegate).save(managedObject: secondary)

        presentationMode.wrappedValue.dismiss()
    


extension AppDelegate 
    /// save and touch related objects
    func save(managedObject: NSManagedObject) 

        let context = persistentContainer.viewContext

        // if this object has an impact on related objects, touch these related objects
        if let secondary = managedObject as? Secondary,
            let primary = secondary.primary 
            context.refresh(primary, mergeChanges: true)
            print("Primary touched: \(primary.primaryName ?? "no name")")
        

        saveContext()
    

【讨论】:

【参考方案5】:

您需要一个发布者,它会生成有关上下文更改的事件和主视图中的一些状态变量,以强制在从该发布者接收事件时重建视图。 重要提示:状态变量必须在视图构建器代码中使用,否则渲染引擎不会知道发生了什么变化。

这是对代码受影响部分的简单修改,它提供了您需要的行为。

@State private var refreshing = false
private var didSave =  NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)

var body: some View 
    List 
        ForEach(fetchedResults)  primary in
            NavigationLink(destination: SecondaryView(primary: primary)) 
                VStack(alignment: .leading) 
                    // below use of .refreshing is just as demo,
                    // it can be use for anything
                    Text("\(primary.primaryName ?? "nil")" + (self.refreshing ? "" : ""))
                    Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
                
            
            // here is the listener for published context event
            .onReceive(self.didSave)  _ in
                self.refreshing.toggle()
            
        
    
    .navigationBarTitle("Primary List")
    .navigationBarItems(trailing:
        Button(action: self.addNewPrimary() ) 
            Image(systemName: "plus")
        
    )

【讨论】:

希望 Apple 在未来改进 Core Data SwiftUI 集成。将赏金授予提供的最佳答案。谢谢阿斯佩里。 感谢您的回答!但是@FetchRequest 应该对数据库中的更改做出反应。使用您的解决方案,无论涉及的项目如何,每次保存在数据库中都会更新视图。我的问题是如何让@FetchRequest 对涉及数据库关系的更改做出反应。您的解决方案需要与@FetchRequest 并行的第二个订阅者(NotificationCenter)。还必须使用额外的假触发器` + (self.refreshing ? "" : "")`。也许@Fetchrequest 本身不是一个合适的解决方案? @Asperi 我接受你的回答。正如您所说,问题在于渲染引擎识别任何更改。使用对已更改对象的引用是不够的。必须在视图中使用已更改的变量。在身体的任何部位。即使在列表的背景上使用也可以。我使用RefreshView(toggle: Bool),其主体中有一个 EmptyView。使用List ....background(RefreshView(toggle: self.refreshing)) 将起作用。 我找到了更好的方法来强制列表刷新/重新获取,它在SwiftUI: List does not update automatically after deleting all Core Data Entity entries 中提供。以防万一。 @g-marc 答案是正确的link

以上是关于当SwiftUI中的相关实体发生变化时,如何更新@FetchRequest?的主要内容,如果未能解决你的问题,请参考以下文章

页面控制器发生了变化,但 Carousel 没有 SwiftUI

当 coreData 值发生变化时,如何更新我的其他视图?

当关系发生变化时,是不是有更新 NSManagedObject 的好方法?

SwiftUI:如何更改用户按住导航链接或按钮等内容时发生的颜色变化?

外部模型对象的 SwiftUI 数据流和 UI 更新,其值发生变化但 id 相同

SwiftUI:如何在函数计算 @State 值时同时更新视图?