core-dataswiftuinsmanagedobjectcontextnsfetchrequestswiftui-navigationlink

Pass FetchedResults through NavigationLink


I have a two CoreData objects:

  1. RoadTrip
  2. StatePlate.

Each RoadTrip items holds an NSSet of StatePlate.

Screen 1 (TripList) shows a list of all RoadTrip items. Screen 2 (StateList) shows a list of all StatePlate items in associated with the RoadTrip that a user selects. Selecting a StatePlate item in Screen 2 will toggle a bool value associated with that item.

Even though I can show the data and can toggle the bool value of each StatePlate, I am not seeing an immediate change to the UI of the screen. The StatePlate should jump from Section to Section in Screen 2 when it's bool value is toggled.

How can I pass this FetchedObject correctly from Screen 1 to Screen 2 so the UI is binded with the data?

Screen 1 (TripList)

struct TripList: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @FetchRequest(entity: RoadTrip.entity(), sortDescriptors: []) var roadTripItems: FetchedResults<RoadTrip>
    
    var body: some View {
        List {
            ForEach(roadTripItems, id: \.self) { trip in
                NavigationLink(destination: StateList(trip: trip)
                    .environment(\.managedObjectContext, self.managedObjectContext)) {
                        TripRow(roadTrip: trip)
                }
            }
        }
    }

}

Screen 2 (StateList)

struct StateList: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    var trip: RoadTrip
    
    var plates: [StatePlate] {
        trip.plateArray
    }
    
    var unseenPlates: [StatePlate] {
        trip.plateArray.filter { !$0.hasBeenSeen }
    }
    
    var seenPlates: [StatePlate] {
        trip.plateArray.filter { $0.hasBeenSeen }
    }

    var body: some View {
        List {
            if !unseenPlates.isEmpty {
                Section(header: Text("Unseen Plates")) {
                    ForEach(unseenPlates, id: \.self) { plate in
                        StateRow(plate: plate)
                    }
                }
            }

            if !seenPlates.isEmpty {
                Section(header: Text("Seen Plates")) {
                    ForEach(seenPlates, id: \.self) { plate in
                        StateRow(plate: plate)
                    }
                }
            }
            
        }
    }
}

StateRow

struct StateRow: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @ObservedObject var plate: StatePlate
    
    var body: some View {
        Button(action: {
            self.plate.hasBeenSeen.toggle()
            try? self.managedObjectContext.save()
        }) {
            HStack {
                Text(String(describing: plate.name!))
                Spacer()
                if plate.hasBeenSeen {
                    Image(systemName: "eye.fill")
                } else {
                    Image(systemName: "")
                }
            }
        }
    }
}

Solution

  • Your trip as object is not changed when plate has changed, so even if it was observed UI was not refreshed.

    Here is possible force-refresh approach.

    struct StateList: View {
        @Environment(\.managedObjectContext) var managedObjectContext
        @ObservedObject var trip: RoadTrip     // << make observed
    
        // .. other code
    

    and add handling for updated plate/s

    StateRow(plate: plate)
      .onReceive(plate.objectWillChange) { _ in
          self.trip.objectWillChange.send()
      }