Как обновить @FetchRequest, когда связанный объект изменяется в SwiftUI?

13

В SwiftUI у Viewменя есть Listоснованный на @FetchRequestпоказе данных Primaryобъекта и Secondaryобъекта, связанного через отношения . ViewИ его Listобновляется корректно, когда я добавить новый Primaryобъект с новым связанным с вторичным объектом.

Проблема в том, что когда я обновляю подключенный Secondaryэлемент в подробном представлении, база данных обновляется, но изменения не отражаются в Primaryсписке. Очевидно, что @FetchRequestэто не вызвано изменениями в другом представлении.

Когда я добавляю новый элемент в основной вид после этого, ранее измененный элемент в конце концов обновляется.

В качестве обходного пути я дополнительно обновляю атрибут Primaryсущности в подробном представлении, и изменения корректно распространяются на Primaryпредставление.

У меня вопрос: как я могу принудительно обновить все связанные @FetchRequestsс SwiftUI Core Data? Особенно, когда у меня нет прямого доступа к связанным объектам / @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 работают правильно в Базовых данных, но не отражаются в пользовательском интерфейсе. Хотел бы получить больше информации об этом.
PJayRushton
Вы пробовали использовать ObservableObject?
kdion4891
Я попытался использовать @ObservedObject var primary: Primary в подробном представлении. Но изменения не распространяются обратно в основной вид.
Бьорн Б.

Ответы:

15

Вам нужен Publisher, который генерировал бы событие об изменениях в контексте и некоторую переменную состояния в первичном представлении, чтобы принудительно перестроить представление при получении события от этого издателя.
Важно: переменная состояния должна использоваться в коде конструктора представлений, иначе механизм визуализации не будет знать, что что-то изменилось.

Вот простая модификация уязвимой части вашего кода, которая дает поведение, которое вам нужно.

@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
источник
Мы надеемся, что Apple улучшит интеграцию Core Data <-> SwiftUI в будущем. Вручение награды за лучший ответ при условии. Спасибо Аспери.
Даррелл Рут
Спасибо за ваш ответ! Но @FetchRequest должен реагировать на изменения в базе данных. С вашим решением, представление будет обновляться при каждом сохранении в базе данных, независимо от участвующих элементов. Мой вопрос состоял в том, как заставить @FetchRequest реагировать на изменения, связанные с отношениями с базой данных. Для вашего решения требуется второй подписчик (NotificationCenter) параллельно с @FetchRequest. Также нужно использовать дополнительный ложный триггер `+ (self.refreshing?" ":" ")`. Может быть, @Fetchrequest сам по себе не подходит?
Бьорн Б.
Да, вы правы, но запрос выборки, созданный в примере, не подвержен изменениям, сделанным в последнее время, поэтому он не обновляется и не обновляется. Может быть, есть причина рассматривать разные критерии запроса на выборку, но это другой вопрос.
Аспери
2
@ Asperi Я принимаю Твой ответ. Как Вы сказали, проблема заключается в том, что движок рендеринга распознает любые изменения. Использование ссылки на измененный объект недостаточно. Измененная переменная должна использоваться в представлении. В любой части тела. Даже используется на фоне в списке будет работать. Я использую RefreshView(toggle: Bool)с одним EmptyView в своем теле. Использование List {...}.background(RefreshView(toggle: self.refreshing))будет работать.
Бьорн Б.
Я нашел лучший способ принудительно обновить / обновить список, он предоставляется в SwiftUI: список не обновляется автоматически после удаления всех записей Core Data Entity . Так, на всякий случай.
Аспери
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's.

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()
    }
}
Бьорн Б.
источник