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

How to update @FetchRequest, when a related Entity changes in SwiftUI?

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

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

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

作为解决方法,我另外更新了详细视图中 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()
    }
}

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

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

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

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

编辑:

基于上述解决方法,我使用全局保存(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()
    }
}

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

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

@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")
        }
    )
}

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

您必须将行包装在单独的视图中,并在实体的该行视图中使用@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()
        }
    }
}

要解决此问题,您必须将 @ObservedObject 添加到 SecondaryView 中的 var primary: Primary 才能正常工作 ListPrimary属于NSManagedObjectclass,已经符合@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()
    }
}

另一种方法:使用 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() 时,它都会为列表视图生成一个新的 UUID,并强制刷新列表视图。

如果你在这里,我没有找到你的视图没有更新的原因,我认为这会对你有所帮助:

  1. 声明核心数据类型时始终使用@ObservedObject。
  2. 如果您使用的是 MVVM,请也使用 @ObservedObject 包装视图模型,并在 VM 中使用 @Published 创建核心数据类型。

这是一个使用@ObservedObject 创建 VM 的示例,因此当核心数据收到更新时,视图模型的实例会重新创建自身,并更新视图。

class ProductTitleValueViewModel: BaseViewModel, ObservableObject {
// MARK: - Properties

@Published var product: Product
var colorSet: [Color]
var currency: Currency

// MARK: - Init

init(product: Product, colorSet: [Color], currency: Currency) {
    self.product = product
    self.colorSet = colorSet
    self.currency = currency
}

}

struct ProductTitleValueView: View {
@ObservedObject var viewModel: ProductTitleValueViewModel

var body: some View {
    VStack(alignment: .leading, spacing: 5) {
        HStack {
            Circle()
                .fill(
                    LinearGradient(colors: viewModel.colorSet, startPoint: .leading, endPoint: .trailing)
                )
                .opacity(0.6)
                .frame(width: 20, height: 20)
            
            Text(viewModel.product.wrappedName)
                .font(.callout.bold())
                .foregroundColor(ThemeColor.lightGray)
        }
        
        
        Text(viewModel.product.balance.toCurrency(with: viewModel.currency))
            .font(.callout.bold())
            .padding(.leading, 28)
        
    }
}
}

如果您遵循这 2 个简单的事情,您将不会遇到核心日期更新视图的问题。