Як оновити @FetchRequest, коли відповідна сутність змінюється в SwiftUI?


13

У SwiftUI Viewя базуюсь Listна @FetchRequestвідображенні даних про Primaryсутність та Secondaryоб'єкт, пов'язаний через зв'язок . ViewІ його Listоновлюється коректно, коли я додати новий Primaryоб'єкт з новим пов'язаним з вторинним об'єктом.

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

Коли після цього я додаю новий елемент у основний вигляд, раніше змінений елемент остаточно оновлюється.

Як вирішення, я додатково оновлюю атрибут Primaryоб'єкта в детальному перегляді, і зміни правильно поширюються на PrimaryПерегляд.

Моє запитання: Як я можу примусити оновити всі пов'язані @FetchRequestsз основними даними SwiftUI? Особливо, коли я не маю прямого доступу до суміжних організацій / @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 basic: Основний у детальному поданні. Але зміни не поширюються на первинний вигляд.
Бьорн Б.

Відповіді:


15

Вам потрібен Видавець, який би генерував подію про зміни контексту та певну змінну стану в первинному вікні, щоб змусити перебудовувати подання при отриманні події від цього видавця.
Важливо: змінна стану повинна використовуватися в коді конструктора подання, інакше двигун візуалізації не знає, що щось змінилося.

Ось проста модифікація зачіпаючої частини вашого коду, яка дає вам необхідну поведінку.

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

Ось, сподіваючись, Apple в майбутньому покращує інтеграцію основних даних <-> SwiftUI. Присудження винагороди за найкращу відповідь. Спасибі Аспері.
Даррелл Корінь

Спасибі за вашу відповідь! Але @FetchRequest повинен реагувати на зміни в базі даних. За допомогою вашого рішення Погляд буде оновлюватися з кожним збереженням у базі даних, незалежно від залучених елементів. Моє запитання полягало в тому, як змусити @FetchRequest реагувати на зміни, пов’язані з відносинами бази даних. Для вашого рішення потрібен другий абонент (NotificationCenter) паралельно @FetchRequest. Також потрібно використовувати додатковий підроблений тригер `+ (self.refreshing?" ":" ")`. Може бути, @Fetchrequest не є підходящим рішенням?
Björn B.

Так, ви маєте рацію, але запит на отримання, як він створений у прикладі, не впливає на зміни, які вносяться останнім часом, тому він не оновлюється / не відновлюється. Можливо, є причина розглянути різні критерії запиту, але це інше питання.
Аспер

2
@Asperi Я приймаю вашу відповідь. Як ви заявили, проблема якось полягає в механізмі візуалізації, щоб визнати будь-які зміни. Використання посилання на змінений Об'єкт недостатньо. Змінена змінна повинна використовуватись у представленні. У будь-якій частині тіла. Навіть використовуваний на тлі в Списку буде працювати. Я використовую a RefreshView(toggle: Bool)з одним EmptyView в його тілі. Використання List {...}.background(RefreshView(toggle: self.refreshing))буде працювати.
Björn B.

Я знайшов кращий спосіб змусити оновити список / оновити список, він надається в SwiftUI: Список не оновлюється автоматично після видалення всіх записів основних даних . Про всяк випадок.
Аспер

1

Я намагався торкнутися основного об’єкта в детальному перегляді так:

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

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

Тоді первинний список оновиться. Але детальний вигляд повинен знати про батьківський об'єкт. Це спрацює, але це, мабуть, не спосіб SwiftUI чи Combine ...

Редагувати:

Виходячи з вищенаведеного рішення, я змінив свій проект глобальною функцією збереження (ManagedObject :). Це торкнеться всіх пов'язаних організацій, таким чином оновивши всі відповідні @ 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()
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.