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

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

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()
    }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.