SwiftUI中相关实体更改时,如何更新@FetchRequest?


13

在SwiftUI中,View我有一个List基于@FetchRequest显示Primary实体数据和关联Secondary实体的Via 关系的数据。在ViewList被正确地更新,当我添加一个新的Primary实体,新的相关辅助实体。

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

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

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

我的问题是:如何@FetchRequests在SwiftUI核心数据中强制所有相关的更新?特别是当我无权直接访问相关实体时@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中。希望获得更多有关此的信息。
PJayRushton

您是否尝试过使用ObservableObject
kdion4891

我尝试在详细视图中使用@ObservedObject var primary:Primary。但是更改不会传播回主视图。
比约恩B.

Answers:


15

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

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

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

希望苹果将来能够改进Core Data <-> SwiftUI集成。将悬赏奖励给提供的最佳答案。谢谢阿斯佩里。
Darrell Root

谢谢您的回答!但是@FetchRequest应该对数据库中的更改做出反应。使用您的解决方案,无论数据库中的每个项目如何保存,视图都会随着数据库中的每次保存而更新。我的问题是如何让@FetchRequest对涉及数据库关系的更改做出反应。您的解决方案需要与@FetchRequest并行的第二个订阅服务器(NotificationCenter)。另外,还必须使用其他伪造触发器`+(self.refreshing?“”:“”)`。也许@Fetchrequest本身不是合适的解决方案?
比约恩B.

是的,您是对的,但是示例中创建的获取请求不受最近所做更改的影响,这就是为什么不对其进行更新/重新获取的原因。可能有理由考虑不同的提取请求条件,但这是不同的问题。
阿斯佩里

2
@Asperi我接受您的回答。如您所述,问题在于渲染引擎无法识别任何更改。使用对已更改对象的引用是不够的。在视图中必须使用已更改的变量。在身体的任何部位。即使在List的背景上使用也可以。我RefreshView(toggle: Bool)在其主体中使用了一个EmptyView。使用List {...}.background(RefreshView(toggle: self.refreshing))将起作用。
比约恩B.

我找到了强制列表刷新/重新引用的更好方法,它在SwiftUI中提供:删除所有核心数据实体条目后,列表不会自动更新。以防万一。
阿斯佩里

1

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

// 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()
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.