Wie aktualisiere ich @FetchRequest, wenn sich eine verwandte Entität in SwiftUI ändert?

13

In einer SwiftUI habe Viewich eine Listbasierend auf @FetchRequestder Anzeige von Daten einer PrimaryEntität und der über eine Beziehung verbundenen SecondaryEntität. Das Viewund sein Listwird korrekt aktualisiert, wenn ich eine neue PrimaryEntität mit einer neuen verwandten sekundären Entität hinzufüge .

Das Problem ist, dass beim Aktualisieren des verbundenen SecondaryElements in einer Detailansicht die Datenbank aktualisiert wird, die Änderungen jedoch nicht in der PrimaryListe berücksichtigt werden. Offensichtlich wird das @FetchRequestnicht durch die Änderungen in einer anderen Ansicht ausgelöst.

Wenn ich danach ein neues Element in der Primäransicht hinzufüge, wird das zuvor geänderte Element endgültig aktualisiert.

Als Problemumgehung aktualisiere ich zusätzlich ein Attribut der PrimaryEntität in der Detailansicht und die Änderungen werden korrekt in die PrimaryAnsicht übertragen.

Meine Frage lautet: Wie kann ich ein Update für alle @FetchRequestsin SwiftUI Core Data verwandten Elemente erzwingen? Insbesondere, wenn ich keinen direkten Zugriff auf die zugehörigen Entitäten habe / @Fetchrequests?

Datenstruktur

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()
    }
}
Björn B.
quelle
Nicht hilfreich, sorry. Aber ich stoße auf dasselbe Problem. Meine Detailansicht enthält einen Verweis auf das ausgewählte primäre Objekt. Es zeigt eine Liste von sekundären Objekten. Alle CRUD-Funktionen funktionieren in Core Data ordnungsgemäß, werden jedoch nicht in der Benutzeroberfläche angezeigt. Würde gerne mehr Infos dazu bekommen.
PJayRushton
Haben Sie versucht, zu verwenden ObservableObject?
kdion4891
Ich habe versucht, @ObservedObject var primary: Primary in der Detailansicht zu verwenden. Die Änderungen werden jedoch nicht in die primäre Ansicht übertragen.
Björn B.

Antworten:

15

Sie benötigen einen Publisher, der ein Ereignis über Änderungen im Kontext und eine Statusvariable in der Primäransicht generiert, um die Neuerstellung der Ansicht beim Empfang eines Ereignisses von diesem Publisher zu erzwingen.
Wichtig: Zustandsvariable muss im View Builder-Code verwendet werden, da sonst die Rendering-Engine nicht weiß, dass sich etwas geändert hat.

Hier finden Sie eine einfache Änderung des betroffenen Teils Ihres Codes, die das von Ihnen benötigte Verhalten ergibt.

@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")
        }
    )
}
Asperi
quelle
Wir hoffen, dass Apple die Integration von Core Data <-> SwiftUI in Zukunft verbessert. Vergabe des Kopfgeldes an die beste Antwort. Danke Asperi.
Darrell Root
Vielen Dank für Ihre Antwort! @FetchRequest sollte jedoch auf Änderungen in der Datenbank reagieren. Mit Ihrer Lösung wird die Ansicht bei jedem Speichern in der Datenbank aktualisiert, unabhängig von den beteiligten Elementen. Meine Frage war, wie ich @FetchRequest dazu bringen kann, auf Änderungen zu reagieren, die Datenbankbeziehungen betreffen. Ihre Lösung benötigt parallel zum @FetchRequest einen zweiten Abonnenten (das NotificationCenter). Außerdem muss man einen zusätzlichen gefälschten Trigger `+ (self.refreshing?" ":" ")` Verwenden. Vielleicht ist eine @Fetchrequest selbst keine geeignete Lösung?
Björn B.
Ja, Sie haben Recht, aber die Abrufanforderung, wie sie im Beispiel erstellt wurde, ist nicht von den Änderungen betroffen, die in letzter Zeit vorgenommen wurden. Deshalb wird sie nicht aktualisiert / erneut abgerufen. Möglicherweise gibt es einen Grund, unterschiedliche Abrufanforderungskriterien zu berücksichtigen, aber das ist eine andere Frage.
Asperi
2
@ Asperi Ich akzeptiere Ihre Antwort. Wie Sie sagten, liegt das Problem irgendwie darin, dass die Rendering-Engine Änderungen erkennt. Die Verwendung eines Verweises auf ein geändertes Objekt reicht nicht aus. Eine geänderte Variable muss in einer Ansicht verwendet werden. In jedem Teil des Körpers. Auch auf einem Hintergrund in der Liste verwendet wird funktionieren. Ich benutze ein RefreshView(toggle: Bool)mit einem einzigen EmptyView in seinem Körper. Verwenden List {...}.background(RefreshView(toggle: self.refreshing))wird funktionieren.
Björn B.
Ich habe einen besseren Weg gefunden, um das Aktualisieren / erneute Abrufen von Listen zu erzwingen. Es wird in SwiftUI bereitgestellt : Die Liste wird nach dem Löschen aller Core Data Entity-Einträge nicht automatisch aktualisiert . Nur für den Fall.
Asperi
1

Ich habe versucht, das primäre Objekt in der Detailansicht folgendermaßen zu berühren:

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

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

Dann wird die primäre Liste aktualisiert. Die Detailansicht muss jedoch über das übergeordnete Objekt Bescheid wissen. Dies wird funktionieren, aber dies ist wahrscheinlich nicht die SwiftUI- oder Combine-Methode ...

Bearbeiten:

Basierend auf der obigen Problemumgehung habe ich mein Projekt mit einer globalen Speicherfunktion (verwalteteObject :) geändert. Dadurch werden alle zugehörigen Entitäten berührt und alle relevanten @ FetchRequest-Daten aktualisiert.

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()
    }
}
Björn B.
quelle