How to update @FetchRequest, when a related Entity changes in SwiftUI?

前端 未结 3 1199
时光说笑
时光说笑 2020-11-30 05:40

In a SwiftUI View i have a List based on @FetchRequest showing data of a Primary entity and the via relationship connecte

相关标签:
3条回答
  • 2020-11-30 06:21

    I also struggled with this and found a very nice and clean solution:

    You have to wrap the row in a separate view and use @ObservedObject in that row view on the entity.

    Here's my code:

    WineList:

    struct WineList: View {
        @FetchRequest(entity: Wine.entity(), sortDescriptors: [
            NSSortDescriptor(keyPath: \Wine.name, ascending: true)
            ]
        ) var wines: FetchedResults<Wine>
    
        var body: some View {
            List(wines, id: \.id) { wine in
                NavigationLink(destination: WineDetail(wine: wine)) {
                    WineRow(wine: wine)
                }
            }
            .navigationBarTitle("Wines")
        }
    }
    

    WineRow:

    struct WineRow: View {
        @ObservedObject var wine: Wine   // !! @ObserveObject is the key!!!
    
        var body: some View {
            HStack {
                Text(wine.name ?? "")
                Spacer()
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-30 06:28

    I tried to touch the primary object in the detail view like this:

    // TODO: ❌ workaround to trigger update on primary @FetchRequest
    
    if let primary = secondary.primary {
       secondary.managedObjectContext?.refresh(primary, mergeChanges: true)
    }
    

    Then the primary list will update. But the detail view has to know about the parent object. This will work, but this is probably not the SwiftUI or Combine way...

    Edit:

    Based on the above workaround, I modified my project with a global save(managedObject:) function. This will touch all related Entities, thus updating all relevant @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()
        }
    }
    
    0 讨论(0)
  • 2020-11-30 06:32

    You need a Publisher which would generate event about changes in context and some state variable in primary view to force view rebuild on receive event from that publisher.
    Important: state variable must be used in view builder code, otherwise rendering engine would not know that something changed.

    Here is simple modification of affected part of your code, that gives behaviour that you need.

    @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")
            }
        )
    }
    
    0 讨论(0)
提交回复
热议问题