Search code examples
core-dataswiftuiobservedobject

CoreData Save not refreshing ListView SwiftUI


I have the following code below and am not sure why the listview is not refreshing on clicking save and dismissing the modal popup.

Content View has the following code which calls NewFlightView modally.

            Button {
                //Show Modal New Flight Window
                isPresented = true
                
            } label: {
                ZStack {
                    Circle ()
                        .foregroundColor(.white)
                        .frame(width: 50, height: 50)
                        .shadow(radius: 4)
                    Image(systemName: "plus.circle.fill")
                        .resizable()
                        .foregroundColor(.cyan)
                        .frame(width: 45, height: 45)
                }
                .offset(y: -15)
            }
            .buttonStyle(TabButtonStyle())
            .sheet(isPresented: $isPresented, onDismiss: {
                flightListVM.getAllFlights()
            }, content: {
                NewFlightView()
            })
            .onAppear(perform: {
                flightListVM.getAllFlights()
            })

The NewFlightView is a simple input view as such

struct NewFlightView: View {

    @StateObject private var newFlightVM = NewFlightViewModel()
    @State private var isPresented: Bool = false

    @Environment(\.presentationMode) var presetationMode

    var body: some View {
        Form {
            DatePicker("Flight Date", selection: $newFlightVM.date)
            Spacer()
            TextField("Enter origin airport", text: $newFlightVM.origin)
                .textFieldStyle(.roundedBorder)
            TextField("Enter destination airport", text: $newFlightVM.destination)
                .textFieldStyle(.roundedBorder)
            TextField("Enter altitude", value: $newFlightVM.altitude, format: .number)
                .textFieldStyle(.roundedBorder)
                .padding()
        
            TextField("Enter flight duration", value: $newFlightVM.duration, format: .number)
                .textFieldStyle(.roundedBorder)
                .padding()
        
                Button ("Save / Get Dose") {
                    newFlightVM.save()
                    presetationMode.wrappedValue.dismiss()
                }
                Spacer()
        }
        .navigationTitle("New Flight")
    
    }
}

Below is the ListView that displays the flights after they are added to CoreData.

struct FlightsListView: View {

    @StateObject private var flightListVM = FlightListViewModel()
    @State private var isPresented: Bool = false

    @AppStorage("isDarkMode") private var isDarkMode: Bool = false
    @State private var isShowingSettings: Bool = false
    @State private var isShowingAddNewFlight: Bool = false
    @State private var animatingButton: Bool = false
    @State var activeSheet: ActiveSheet?

   // @ObservedObject private var appSetting = AppSetting.shared

    private func deleteFlight(at indexSet: IndexSet) {
        indexSet.forEach { index in
            let flight = flightListVM.flights[index]
            //delete the flight
            flightListVM.deleteFlight(flight: flight)
        
            //get all flights
            flightListVM.getAllFlights()
        }
    }

    var body: some View {
        //Displays all flights in the database
        List {
            ForEach(flightListVM.flights, id: \.id) { flight in
                NavigationLink(destination: FlightDetailView(flight: flight )) {
                    FlightCell(flight: flight)
                } //: Link
            
            }.onDelete(perform: deleteFlight)
        }
        .listStyle(PlainListStyle())
        .navigationTitle("Flights")
  
        .sheet(isPresented: $isPresented, onDismiss: {
            flightListVM.getAllFlights()
        }, content: {
            NewFlightView()
        })
    
        .onAppear {
            flightListVM.getAllFlights()
        }
    }
} 

And finally the ViewModels that I am using to control the MVVM side.

NewFlightViewModel

class NewFlightViewModel: ObservableObject {

    var date: Date = Date()
    var origin: String = ""
    var destination: String = ""
    var altitude: Double = 0.0
    var duration: Double = 0.0

    func save() {
    
        let manager = CoreDataManager.shared
        let flight = Flight(context: manager.persistentContainer.viewContext)
    
        flight.date = date
        flight.origin = origin
        flight.destination = destination
        flight.altitude = altitude
        flight.duration = duration
    
        manager.save()
    
    }
}

FlightListViewModel

class FlightListViewModel: ObservableObject {

    @Published var flights = [FlightViewModel]()

    func deleteFlight(flight: FlightViewModel) {
        let flight = CoreDataManager.shared.getFlightById(id: flight.id)
        if let flight = flight {
            CoreDataManager.shared.deleteFlight(flight)
        }
    }

    func getAllFlights () {
    
        let flights = CoreDataManager.shared.getAllFlights()
        DispatchQueue.main.async {
            self.flights = flights.map(FlightViewModel.init)
        } 
    }
}


struct FlightViewModel {

    let flight: Flight

    var id: NSManagedObjectID {
        return flight.objectID
    }

    var date: String? {
        return flight.date?.asFormattedString()
    }

    var origin: String {
        return flight.origin ?? ""
    }

    var destination: String {
        return flight.destination ?? ""
    }

    var altitude: Double? {
        return Double(flight.altitude)
    }

    var duration: Double? {
        return Double(flight.duration)
    }
}

Okay. I need some guidance as to why when dismissing the model NewFlightView and it triggers the onDismiss handle of the ContentView. How can I refresh the FlightListView without having to click on another tab and then back to the Flights tab thus triggering its onAppear event?


Solution

  • Remove the view model object (we don't use MVVM in SwiftUI) and use @FetchRequest. That will call body automatically when the results change.

    Move your helpers to an extension of NSManagedObjectContext and use it from the View struct via @Environment(\.managedObjectContext) var viewContext.

    Looks to me like you are missing your NSManagedObject file for Flight. Generate it in the model editor choosing Editor->Create NSManagedObject subclass. Choose Codegen Category/Extension. Put your computed properties in there, but don't do any formatting that must go in body otherwise SwiftUI won't be able to update UILabels when region settings change.