xcodeswiftui

SwiftUI- force refresh of viewModel when database loaded inside this viewModel changes


Complete SwiftUI beginner here. I started to play with SwiftUI using Nick Sarno aka "Swiftful Thinking" project (https://github.com/SwiftfulThinking/SwiftfUI-Map-App-MVVM). This project is a Map app with NVMM structure. It consists of database of locations and shows them on user-friendly interface consisting of Map, locations list and detail view overlay. I really like the interface and smooth feeling of this app.

There is viewModel used as @EnvironmentObject in all child views (LocationsViewModel.swift). Inside this model the database consisting of array of structs (stored as static let in LocationsDataService.swift file) is initialized and loaded into variable called locations (line 35 of LocationsViewModel.swift).

init() {
    let locations = LocationsDataService.locations
    self.locations = locations
    self.mapLocation = locations.first!
    self.updateMapRegion(location: locations.first!)
}

I am trying to update the database with additional locations on the runtime, but I can't figure out how to reload views to show new data on the list of locations without app restart. From my complete beginner point of view LocationsViewModel is set as ObservableObject, so any change to it should force views observing it to redraw, but it is not occurring. I even tried to "re-init" the view model to once again load database into let, but no luck..

Can you advice and point me into right direction how to force app to refresh itself when Locations database is changed (either new locations are added or some are removed)?

Edit - More details:

I am updating LocationsDataService.locations from local JSON file which I create/update upon receiving it from my Apple Watch. File is stored in AppSupportDirectory. If my JSON file is not empty and is correctly written, then its contents are decoded and loaded into LocationsViewModel().locations using below function:

class DataManager: ObservableObject{

    func LoadFromJson()  {
            do{
                let fileUrl = pathForAppSupportDirectoryAsUrl()?.appendingPathComponent("pinData.JSON")
                let fileData = try Data(contentsOf: (fileUrl)!)
                if !fileData.isEmpty {
                    let arrayOfPins = try JSONDecoder().decode([Location].self, from: fileData)
                    LocationsViewModel().locations = arrayOfPins
                }
                
            }catch{
                print(error)
            }
        }

    func pathForAppSupportDirectoryAsUrl() -> URL?{
        var appSupportDirectory: URL?
        do{
            appSupportDirectory = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        }catch{
            print("problem!")
        }
        return appSupportDirectory
    }
}

LoadFromJson() is called after the file is successfully encoded and saved inside app support directory. I know that writing to local file and then decoding it works, as my locations dataset is correctly updated if I kill the app and start it again.

Thank you so much

Karol


Solution

  • I recommend separating LocationsViewModel from DataManager and just have DataManager return the decoded Location array:

    final class DataManager {
    
        func loadLocationPinsFromJson() throws -> [Location]  {
            let fileUrl = pathForAppSupportDirectoryAsUrl()?.appendingPathComponent("pinData.JSON")
            let fileData = try Data(contentsOf: (fileUrl)!)
            return try JSONDecoder().decode([Location].self, from: fileData)
        }
    
        private func pathForAppSupportDirectoryAsUrl() -> URL? {
            try? FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        }
    }
    

    In LocationsViewModel you could initialize an instance of DataManager or pass in a shared instance and use it to load the Location array when LocationsListView appears:

    final class LocationsViewModel: ObservableObject {
        @Published var locations = [Location]()
        private let dataManager = DataManager() // initialize here or pass in shared instance
    
        func loadLocations() {
            do {
                locations = try dataManager.loadLocationPinsFromJson()
            } catch {
                // handle loading error...
            }
        }
    }
    

    Here LocationsListView initializes LocationsViewModel as a StateObject and calls viewModel.loadLocations in the onAppear modifier:

    struct LocationsListView: View {
        @StateObject private var viewModel = LocationsViewModel()
    
        var body: some View {
            Text("Locations List Here (viewModel.locations)...")
                .onAppear(perform: viewModel.loadLocations)
        }
    }