วิธีการอัพเดต @FetchRequest เมื่อมีการเปลี่ยนแปลง Entity ที่เกี่ยวข้องใน 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 ทั้งหมดทำงานอย่างถูกต้องใน Core Data แต่ไม่ได้สะท้อนใน UI ชอบที่จะได้รับข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้
PJayRushton

คุณเคยลองใช้ObservableObject?
kdion4891

ฉันลองใช้ @ObservedObject var primary: Primary ในมุมมองรายละเอียด แต่การเปลี่ยนแปลงนั้นจะไม่แพร่กระจายกลับไปสู่มุมมองหลัก
Björn B.

คำตอบ:


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 จะปรับปรุงการรวม Core Data <-> SwiftUI ในอนาคต มอบรางวัลให้กับคำตอบที่ดีที่สุด ขอบคุณ Asperi
ดาร์เรลรูต

ขอบคุณสำหรับคำตอบ! แต่ @FetchRequest ควรตอบสนองต่อการเปลี่ยนแปลงในฐานข้อมูล ด้วยโซลูชันของคุณมุมมองจะได้รับการอัปเดตทุกครั้งที่บันทึกในฐานข้อมูล คำถามของฉันคือวิธีรับ @FetchRequest เพื่อตอบสนองต่อการเปลี่ยนแปลงที่เกี่ยวข้องกับความสัมพันธ์ของฐานข้อมูล โซลูชันของคุณต้องการผู้สมัครสมาชิกรายที่สอง (NotificationCenter) ขนานกับ @FetchRequest นอกจากนี้ยังมีการใช้ทริกเกอร์ปลอมเพิ่มเติม `+ (self.refreshing?" ":" ")` บางที @Fetchrequest ไม่ใช่โซลูชันที่เหมาะสมใช่ไหม
Björn B.

ใช่คุณถูกต้อง แต่คำขอดึงข้อมูลตามที่สร้างขึ้นในตัวอย่างไม่ได้รับผลกระทบจากการเปลี่ยนแปลงที่เกิดขึ้นเมื่อเร็ว ๆ นี้นั่นคือสาเหตุที่ไม่ได้รับการปรับปรุง / ดึงข้อมูล อาจมีเหตุผลในการพิจารณาเกณฑ์คำขอดึงข้อมูลที่แตกต่างกัน แต่นั่นเป็นคำถามที่แตกต่างกัน
Asperi

2
@ Asperi ฉันยอมรับคำตอบของคุณ ตามที่คุณระบุปัญหาอยู่ที่เอ็นจินการเรนเดอร์เพื่อรับรู้การเปลี่ยนแปลงใด ๆ การใช้การอ้างอิงไปยังวัตถุที่ถูกเปลี่ยนนั้นไม่เพียงพอ จะต้องใช้ตัวแปรที่เปลี่ยนแปลงในมุมมอง ในส่วนใดของร่างกาย แม้ใช้บนพื้นหลังในรายการจะทำงาน ฉันใช้ a RefreshView(toggle: Bool)พร้อมกับ EmptyView เดียวในร่างกาย List {...}.background(RefreshView(toggle: self.refreshing))จะใช้งานได้
Björn B.

ฉันได้พบวิธีที่ดีกว่าที่จะบังคับให้รีเฟรชรายการ / แหล่งข้อมูลนั้นอีกก็จะถูกจัดให้อยู่ในSwiftUI: รายชื่อไม่ได้ปรับปรุงโดยอัตโนมัติหลังจากลบทุกรายการหลักข้อมูล ในกรณีที่
Asperi

1

ฉันพยายามที่จะสัมผัสวัตถุหลักในมุมมองรายละเอียดเช่นนี้:

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

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

จากนั้นรายการหลักจะอัปเดต แต่มุมมองรายละเอียดต้องรู้เกี่ยวกับวัตถุแม่ สิ่งนี้จะใช้งานได้ แต่อาจไม่ใช่วิธีของ SwiftUI หรือวิธีรวม ...

แก้ไข:

จากการแก้ปัญหาข้างต้นฉันแก้ไขโครงการของฉันด้วยฟังก์ชั่นการบันทึกทั่วโลก (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()
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.