How do I pass a MKMapItem from a view to the Google Maps coordinator in SwiftUI?

166 Views Asked by At

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
    }
}
1

There are 1 best solutions below

3
malhal On

makeUIView needs to do the making, e.g.

    //let mapView = GMSMapView(frame: .zero)

    func makeUIView(context: Context) -> GMSMapView {
        return GMSMapView(frame: .zero) // mapView
    }

self is a value, not a reference so you need to fix your coordinator init, e.g.

    func makeCoordinator() -> MapCoordinator {
        return MapCoordinator() // mapView: self)
    }

We don't use view model objects in SwiftUI, you need to learn the features of the View struct, e.g. to do an async search it's like this:

struct SearchView: View {

    let query: String

    @State var results: [ResultType]

    var body: some View {
        ...
        .task(id: query) {
            if query == "" {
                results = []
                return
            }
            let localSearch = ..
            let response = await localSearch.start()
            results = ...
        }
    }

The task is started on appear, cancelled and restarted if id changes, and cancelled on disappear. You can't do this in custom view model objects that are hacked into a state object.