Bagaimana cara memperbarui @FetchRequest, ketika Entitas terkait berubah di SwiftUI?

13

Dalam SwiftUI Viewsaya memiliki Listberdasarkan pada @FetchRequestmenunjukkan data suatu Primaryentitas dan hubungan yang terhubung Secondaryentitas. The Viewdan yang Listdiperbarui dengan benar, ketika saya menambahkan baru Primaryentitas dengan entitas sekunder terkait baru.

Masalahnya adalah, ketika saya memperbarui Secondaryitem yang terhubung dalam tampilan detail, database akan diperbarui, tetapi perubahannya tidak tercermin dalam PrimaryDaftar. Jelas, @FetchRequestitu tidak dipicu oleh perubahan di tampilan lain.

Ketika saya menambahkan item baru di tampilan utama setelahnya, item yang sebelumnya diubah akhirnya diperbarui.

Sebagai solusinya, saya juga memperbarui atribut Primaryentitas dalam tampilan detail dan perubahan menyebar dengan benar ke tampilan Primary.

Pertanyaan saya adalah: Bagaimana saya bisa memaksa pembaruan pada semua yang terkait @FetchRequestsdalam Data Inti SwiftUI? Terutama, ketika saya tidak memiliki akses langsung ke entitas terkait / @Fetchrequests?

Struktur data

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.
sumber
Tidak membantu, maaf. Tapi saya mengalami masalah yang sama. Tampilan detail saya memiliki referensi ke objek utama yang dipilih. Ini menunjukkan daftar objek sekunder. Semua fungsi CRUD bekerja dengan baik di Core Data tetapi tidak tercermin di UI. Ingin mendapatkan info lebih lanjut tentang ini.
PJayRushton
Sudahkah Anda mencoba menggunakan ObservableObject?
kdion4891
Saya mencoba menggunakan @ObservedObject var primary: Primary pada tampilan detail. Tetapi perubahan tidak menyebar kembali ke tampilan utama.
Björn B.

Jawaban:

15

Anda memerlukan Penerbit yang akan menghasilkan acara tentang perubahan dalam konteks dan beberapa variabel status dalam tampilan utama untuk memaksa tampilan dibangun kembali saat menerima acara dari penerbit itu.
Penting: variabel keadaan harus digunakan dalam kode pembangun tampilan, jika tidak mesin rendering tidak akan tahu bahwa ada sesuatu yang berubah.

Berikut ini adalah modifikasi sederhana dari bagian yang terpengaruh dari kode Anda, yang memberikan perilaku yang Anda butuhkan.

@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
sumber
Di sini diharapkan Apple meningkatkan Core Data <-> Integrasi SwiftUI di masa depan. Memberi hadiah untuk jawaban terbaik yang disediakan. Terima kasih Asperi.
Darrell Root
Terima kasih atas jawaban Anda! Tapi @FetchRequest harus bereaksi terhadap perubahan dalam database. Dengan solusi Anda, Tampilan akan diperbarui dengan setiap penyimpanan di database, terlepas dari item yang terlibat. Pertanyaan saya adalah bagaimana agar @FetchRequest bereaksi terhadap perubahan yang melibatkan hubungan basis data. Solusi Anda membutuhkan pelanggan kedua (NotificationCenter) secara paralel dengan @FetchRequest. Kita juga harus menggunakan pemicu palsu tambahan `+ (self.refreshing?" ":" ")`. Mungkin @Fetchrequest bukan solusi yang cocok?
Björn B.
Ya, Anda benar, tetapi permintaan pengambilan seperti yang dibuat pada contoh tidak terpengaruh oleh perubahan yang dibuat akhir-akhir ini, itu sebabnya permintaan tersebut tidak diperbarui / diambil ulang. Mungkin ada alasan untuk mempertimbangkan kriteria pengambilan yang berbeda, tetapi itu adalah pertanyaan yang berbeda.
Asperi
2
@Saya berpengalaman, saya menerima jawaban Anda. Seperti yang Anda nyatakan, masalahnya terletak pada mesin rendering untuk mengenali perubahan apa pun. Menggunakan referensi ke Obyek yang diubah tidak cukup. Variabel yang diubah harus digunakan dalam Tampilan. Di bagian mana pun dari tubuh. Bahkan digunakan pada latar belakang pada Daftar akan berhasil. Saya menggunakan RefreshView(toggle: Bool)dengan EmptyView tunggal di tubuhnya. Menggunakan List {...}.background(RefreshView(toggle: self.refreshing))akan berhasil.
Björn B.
Saya telah menemukan cara yang lebih baik untuk memaksa Daftar refresh / refetch, disediakan di SwiftUI: Daftar tidak memperbarui secara otomatis setelah menghapus semua entri Entitas Data Inti . Untuk berjaga-jaga.
Asperi
1

Saya mencoba menyentuh objek utama dalam tampilan detail seperti ini:

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

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

Kemudian daftar utama akan diperbarui. Tetapi tampilan detail harus tahu tentang objek induk. Ini akan berhasil, tetapi ini mungkin bukan cara SwiftUI atau Gabungkan ...

Edit:

Berdasarkan solusi di atas, saya memodifikasi proyek saya dengan fungsi save global (managedObject :). Ini akan menyentuh semua Entitas terkait, sehingga memperbarui semua @ FetchRequest yang relevan.

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.
sumber