Come aggiornare @FetchRequest, quando un'entità correlata cambia in SwiftUI?


13

In SwiftUI Viewho Listbasato sulla @FetchRequestvisualizzazione dei dati di Primaryun'entità e l' Secondaryentità collegata tramite relazione . Il Viewe il suo Listviene aggiornato correttamente, quando aggiungo un nuovo Primarysoggetto con una nuova entità secondaria correlata.

Il problema è che quando aggiorno l' Secondaryelemento connesso in una vista di dettaglio, il database viene aggiornato, ma le modifiche non si riflettono Primarynell'elenco. Ovviamente, il @FetchRequestnon viene attivato dalle modifiche in un'altra vista.

Successivamente, quando aggiungo un nuovo elemento nella vista principale, l'elemento precedentemente modificato viene finalmente aggiornato.

Per ovviare al problema, aggiorno inoltre un attributo Primarydell'entità nella vista di dettaglio e le modifiche si propagano correttamente alla Primaryvista.

La mia domanda è: come posso forzare un aggiornamento su tutti i relativi @FetchRequestsdati core di SwiftUI? In particolare, quando non ho accesso diretto alle entità correlate / @Fetchrequests?

Struttura dati

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()
    }
}

Non utile, scusa. Ma sto incontrando questo stesso problema. La mia vista di dettaglio ha un riferimento all'oggetto primario selezionato. Mostra un elenco di oggetti secondari. Tutte le funzioni CRUD funzionano correttamente nei Core Data ma non si riflettono nell'interfaccia utente. Mi piacerebbe avere maggiori informazioni su questo.
PJayRushton,

Hai provato a usare ObservableObject?
kdion4891

Ho provato a utilizzare @ObservedObject var primary: Primary nella visualizzazione dettagliata. Ma le modifiche non si propagano nuovamente nella vista principale.
Björn B.

Risposte:


15

È necessario un editore che generi un evento sui cambiamenti nel contesto e alcune variabili di stato nella vista primaria per forzare la ricostruzione della vista sull'evento di ricezione da quell'editore.
Importante: la variabile di stato deve essere utilizzata nel codice del builder vista, altrimenti il ​​motore di rendering non saprebbe che qualcosa è cambiato.

Ecco una semplice modifica della parte interessata del codice, che fornisce il comportamento di cui hai bisogno.

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

Speriamo che Apple migliori l'integrazione dei dati Core <-> SwiftUI in futuro. Assegnare la generosità alla migliore risposta fornita. Grazie Asperi.
Darrell Root,

La ringrazio per la risposta! Ma @FetchRequest dovrebbe reagire alle modifiche nel database. Con la tua soluzione, la vista verrà aggiornata ad ogni salvataggio nel database, indipendentemente dagli elementi coinvolti. La mia domanda era come far reagire @FetchRequest alle modifiche che coinvolgono le relazioni con il database. La tua soluzione necessita di un secondo abbonato (NotificationCenter) in parallelo a @FetchRequest. Inoltre si deve usare un ulteriore falso trigger `+ (self.refreshing?" ":" ")`. Forse un @Fetchrequest non è una soluzione adatta in sé?
Björn B.

Sì, hai ragione, ma la richiesta di recupero così come viene creata nell'esempio non è influenzata dalle modifiche apportate di recente, ecco perché non viene aggiornata / recuperata. Potrebbe esserci un motivo per considerare criteri di richiesta di recupero diversi, ma questa è una domanda diversa.
Asperi,

2
@Asperi Accetto la tua risposta. Come hai affermato, il problema si trova in qualche modo nel motore di rendering per riconoscere eventuali modifiche. L'uso di un riferimento a un oggetto modificato non è sufficiente. Una variabile modificata deve essere utilizzata in una vista. In qualsiasi parte del corpo. Anche usato su uno sfondo dell'elenco funzionerà. Uso un RefreshView(toggle: Bool)con un singolo EmptyView nel suo corpo. L'uso List {...}.background(RefreshView(toggle: self.refreshing))funzionerà.
Björn B.

Ho trovato il modo migliore per forzare l'aggiornamento / il recupero dell'elenco , è fornito in SwiftUI: l'elenco non si aggiorna automaticamente dopo aver eliminato tutte le voci dell'entità dati principali . Nel caso in cui.
Asperi,

1

Ho provato a toccare l'oggetto principale nella vista di dettaglio in questo modo:

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

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

Quindi l'elenco principale verrà aggiornato. Ma la vista di dettaglio deve conoscere l'oggetto principale. Funzionerà, ma questo probabilmente non è il modo SwiftUI o Combina ...

Modificare:

Sulla base della soluzione precedente, ho modificato il mio progetto con una funzione di salvataggio globale (managedObject :). Questo toccherà tutte le Entità correlate, aggiornando così tutte le @ FetchRequest rilevanti.

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()
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.