Jak zaktualizować @FetchRequest, gdy powiązana jednostka zmienia się w SwiftUI?

13

W SwiftUI Viewmam polegać Listna @FetchRequestpokazywaniu danych Primaryjednostki i jednostki powiązanej za pośrednictwem relacji Secondary. ViewI jej Listprawidłowo aktualizowany, gdy dodać nowy Primarypodmiot z nowym podmiotem powiązanym wtórnym.

Problem polega na tym, że kiedy aktualizuję podłączony Secondaryelement w widoku szczegółowym, baza danych jest aktualizowana, ale zmiany nie są odzwierciedlone na Primaryliście. Oczywiście zmiany @FetchRequestnie są wywoływane przez zmiany w innym widoku.

Kiedy dodam nowy element w głównym widoku, poprzednio zmieniony element zostanie ostatecznie zaktualizowany.

Aby obejść ten problem, dodatkowo aktualizuję atrybut Primaryobiektu w widoku szczegółów, a zmiany są poprawnie propagowane do Primarywidoku.

Moje pytanie brzmi: jak mogę wymusić aktualizację wszystkich powiązanych danych @FetchRequestsw SwiftUI Core Data? Zwłaszcza gdy nie mam bezpośredniego dostępu do powiązanych podmiotów / @Fetchrequests?

Struktura danych

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.
źródło
Przepraszam, nie jest pomocny. Ale napotykam ten sam problem. Mój widok szczegółów zawiera odniesienie do wybranego obiektu podstawowego. Pokazuje listę obiektów wtórnych. Wszystkie funkcje CRUD działają poprawnie w podstawowych danych, ale nie są odzwierciedlone w interfejsie użytkownika. Chciałbym uzyskać więcej informacji na ten temat.
PJayRushton
Próbowałeś używać ObservableObject?
kdion4891
Próbowałem użyć @ObservedObject var primary: Primary w widoku szczegółów. Ale zmiany nie są przenoszone z powrotem do głównego widoku.
Björn B.,

Odpowiedzi:

15

Potrzebujesz wydawcy, który generowałby zdarzenie dotyczące zmian kontekstu i pewnej zmiennej stanu w widoku podstawowym, aby wymusić przebudowę widoku przy zdarzeniu odbierania od tego wydawcy.
Ważne: zmienna stanu musi być używana w kodzie konstruktora widoku, w przeciwnym razie silnik renderujący nie wiedziałby, że coś się zmieniło.

Oto prosta modyfikacja dotkniętej części kodu, która zapewnia zachowanie, którego potrzebujesz.

@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
źródło
Mamy nadzieję, że Apple ulepszy integrację Core Data <-> SwiftUI w przyszłości. Przyznanie nagrody za najlepszą podaną odpowiedź. Dzięki Asperi.
Darrell Root
Dziękuję za Twoją odpowiedź! Ale @FetchRequest powinien reagować na zmiany w bazie danych. Dzięki Twojemu rozwiązaniu Widok będzie aktualizowany przy każdym zapisie w bazie danych, niezależnie od zaangażowanych elementów. Moje pytanie brzmiało, jak sprawić, by @FetchRequest zareagował na zmiany dotyczące relacji z bazą danych. Twoje rozwiązanie potrzebuje drugiego subskrybenta (NotificationCenter) równolegle do @FetchRequest. Trzeba również użyć dodatkowego fałszywego wyzwalacza `+ (self.refreshing?" ":" ")`. Może sam @Fetchrequest nie jest odpowiednim rozwiązaniem?
Björn B.,
Tak, masz rację, ale na żądanie pobierania, jak jest tworzone na przykład, nie mają wpływu zmiany, które zostały ostatnio wprowadzone, dlatego nie są aktualizowane / ponownie pobierane. Być może istnieje powód, aby rozważyć inne kryteria żądania pobierania, ale to inne pytanie.
Asperi
2
@Asperi Akceptuję Twoją odpowiedź. Jak już wspomniałeś, problem polega na tym, że silnik renderujący rozpoznaje wszelkie zmiany. Użycie odniesienia do zmienionego obiektu nie wystarczy. Zmienną zmienną należy użyć w widoku. W dowolnej części ciała. Nawet użyty w tle na liście będzie działać. Używam RefreshView(toggle: Bool)z jednym EmptyView w jego ciele. Korzystanie List {...}.background(RefreshView(toggle: self.refreshing))będzie działać.
Björn B.,
Znalazłem lepszy sposób na wymuszenie odświeżenia / ponownego pobrania listy, jest on dostępny w SwiftUI: lista nie aktualizuje się automatycznie po usunięciu wszystkich wpisów podstawowej jednostki danych . W razie czego.
Asperi
1

Próbowałem dotknąć głównego obiektu w widoku szczegółowym w następujący sposób:

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

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

Następnie zaktualizowana zostanie lista podstawowa. Ale widok szczegółów musi wiedzieć o obiekcie nadrzędnym. To zadziała, ale prawdopodobnie nie jest to sposób SwiftUI lub Combine ...

Edytować:

W oparciu o powyższe obejście zmodyfikowałem swój projekt za pomocą globalnej funkcji składowania (manageObject :). Spowoduje to dotknięcie wszystkich powiązanych jednostek, a tym samym zaktualizowanie wszystkich odpowiednich @ 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()
    }
}
Björn B.
źródło