【问题标题】:How to update @FetchRequest, when a related Entity changes in SwiftUI?当SwiftUI中的相关实体发生变化时,如何更新@FetchRequest?
【发布时间】: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。但更改不会传播回主视图。

标签: core-data swiftui combine


【解决方案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
        猜你喜欢
        • 2021-01-17
        • 1970-01-01
        • 2022-01-07
        • 2020-08-24
        • 1970-01-01
        • 1970-01-01
        • 2018-10-30
        • 2023-01-04
        • 2018-10-16
        相关资源
        最近更新 更多