I'm new to SwiftUI and trying to create a Google Maps-based iOS app that will allow the user to search for a location from a "search" view using MapKit's search completion. Selecting the location would show the details on an "information" view with a Save button that, if clicked, would add a marker on the map. Some of the code includes Swift due to Google Maps not supporting SwiftUI.
My problem is how to implement the action of the Save button that calls the addLocation() helper in the Google Maps coordinator.
If I add @Published var locationToAdd: MKMapItem? to SearchLocationViewModel and @EnvironmentObject var viewModel: SearchLocationViewModel to GoogleMapViewRepresentable it would call updateUIView() and I could add if viewModel.locationToAdd condition to then add the marker, but updateUIView() would then also be triggered when I'm typing a search or selecting a search completion result because the observed SearchLocationViewModel gets updated.
If I set viewModel.locationToAdd = nil right after adding the marker in addLocation() I get an error (nil cannot initialize specified type MKMapItem), but even if I didn't, that would trigger another update to SearchLocationViewModel and another updateUIView() call (I'm not sure if this would be an issue, but it seems sloppy).
Is the correct solution to just create another view model that only includes a single @Published var locationToAdd: MKMapItem? and keep it separate from SearchLocationViewModel?
Relevant code:
GoogleMapViewRepresentable:
import SwiftUI
import MapKit
import GoogleMaps
struct GoogleMapViewRepresentable: UIViewRepresentable {
let mapView = GMSMapView(frame: .zero)
func makeCoordinator() -> MapCoordinator {
return MapCoordinator(mapView: self)
}
func makeUIView(context: Context) -> GMSMapView {
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
}
}
extension GoogleMapViewRepresentable {
class MapCoordinator: NSObject, CLLocationManagerDelegate {
var parent: GoogleMapViewRepresentable
init(mapView: GoogleMapViewRepresentable) {
parent = mapView
}
func addLocation(_ item: MKMapItem?) {
print("Item: \(item)")
}
}}
}
SearchLocationViewModel:
import MapKit
class SearchLocationViewModel: NSObject, ObservableObject {
@Published var results = [MKLocalSearchCompletion]()
@Published var selectedLocationResponseItem: MKMapItem?
private let searchCompleter = MKLocalSearchCompleter()
var queryFragment: String = "" {
didSet {
searchCompleter.queryFragment = queryFragment
}
}
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.queryFragment = queryFragment
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address, .pointOfInterest])
}
func searchLocation(forLocalSearchCompletion completion: MKLocalSearchCompletion, completionHandler: @escaping MKLocalSearch.CompletionHandler) {
let searchRequest = MKLocalSearch.Request(completion: completion)
let search = MKLocalSearch(request: searchRequest)
search.start(completionHandler: completionHandler)
}
func selectLocation(_ localSearch: MKLocalSearchCompletion) {
searchLocation(forLocalSearchCompletion: localSearch) { response, error in
if let error = error {
print("Error: \(error.localizedDescription)")
return
}
guard let item = response?.mapItems.first else { return }
self.selectedLocationResponseItem = item
}
}
}
extension SearchLocationViewModel: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
self.results = completer.results
}
}
makeUIViewneeds to do the making, e.g.selfis a value, not a reference so you need to fix your coordinator init, e.g.We don't use view model objects in SwiftUI, you need to learn the features of the
Viewstruct, e.g. to do an async search it's like this:The task is started on appear, cancelled and restarted if
idchanges, and cancelled on disappear. You can't do this in custom view model objects that are hacked into a state object.