swiftswiftui

How to pass context or update SwiftData in ViewModel


I have a SwiftData object called UserLocation that I want to update with a new item anytime a button is clicked that gets the users current location. I have a ViewModel that currently gets the location and adds it to an array but I can't figure out how to pass the ModelContext to my ViewModel to update. Or is there a way to track the changes in my @Published variable in the view to update the ModelContext?

import SwiftData
import MapKit

struct LocationTestView: View {
    
    @Environment(\.modelContext) private var modelContext
    @Query private var userLocations: [UserLocation]
    @StateObject private var viewModel = LocationTestViewModel()
    
    var body: some View {
        ZStack(alignment: .bottom) {
            List {
                ForEach(userLocations) { userLocation in
                  //add view data
                }
            }
            Button(action: addUserLocation, label: {
                Text("Get Location")
            })
        }
    }
    
    private func addUserLocation() {
        withAnimation {
            viewModel.getCurrentLocation()
        }
    }
}

final class LocationTestViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
    let locationManager = CLLocationManager()
    
    @Published var coordinates = [CLLocationCoordinate2D]()
    
    override init() {
        super.init()
        locationManager.delegate = self
    }
    
    func getCurrentLocation() {
        locationManager.requestLocation()
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let latestLocation = locations.first else {
            return
        }
        
        DispatchQueue.main.async {
            self.coordinates.append(latestLocation.coordinate); //Need to update userLocations instead
        }
        
        print(coordinates)
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
        print(error)
    }
}

Solution

  • In SwiftUI the View struct hierarchy is the view model already shouldn't try to put view data into objects, those are designed for model or fetch controllers where you need reference semantics (like delegation in your case). This is because the View struct hierarchy is designed to efficiently track changes to your model/fetched data and call body to make new structs (which are then diffed by SwiftUI to create/update the actual UI objects). The View structs are lightweight, descriptor type values on the memory stack, there is no issue with making lots of them quickly, same way you wouldn't care about making multiple ints. Structs for view data are more efficient and less likely to have consistency mistakes compared with view model objects.

    I recommend renaming your LocationTestViewModel to LocationFetcher and make its only job to retrieve a location. Then in your View struct use that location to update your SwiftData code using the context from the environment. You can put the logic for this in your SwiftData model class or in an extension on the container to make it testable.

    Note, you will also need a LocationAuthorizer state object in a parent AuthorizationView that asks for authorization and thus only show this LocationTestView when authorized and hide it again when de-authorized.