Search code examples
swiftuigoogle-maps-sdk-ios

How to invoke a method in a view in SwiftUI


Just getting started with SwiftUI.

I have a GoogleMapsView in a ContentView using the CLLocationManager I capture events in the AppDelegate or SceneDelegate class by means of extending them with CLLocationManagerDelegate.

How can I invoke a method in the GoogleMapsView from the AppDelegate or SceneDelegate?

In this instance I want to call the .animate method when the location change event is sent to the AppDelegate instance via the CLLocationManagerDelegate, but the question is really more generic.


Solution

  • I made and implementation of CLLocationManager and MKMapView and it is almost the same as maps, hope it will help you:

    Short answer: declaring a @Binding var foo: Any you will be able to make changes inside GoogleMapView every time that foo changes, in this case foo is your location, so you can call animate every time foo is updated.

    Long answer:

    First I created a Mapview that conforms UIViewRepresentable protocol, just as you did, but adding a @Binding variable, this is my "trigger".

    MapView:

    struct MapView: UIViewRepresentable {
        @Binding var location: CLLocation // Create a @Binding variable that keeps the location where I want to place the view, every time it changes updateUIView will be called
        private let zoomMeters = 400
    
        func makeUIView(context: UIViewRepresentableContext<MapView>) -> MKMapView {
            let mapView = MKMapView(frame: UIScreen.main.bounds)
            return mapView
        }
    
        func updateUIView(_ mapView: MKMapView, context: Context) {
            //When location changes, updateUIView is called, so here I move the map:
            let region = MKCoordinateRegion(center: location.coordinate,
                                            latitudinalMeters: CLLocationDistance(exactly: zoomMeters)!,
                                            longitudinalMeters: CLLocationDistance(exactly: zoomMeters)!)
            mapView.setRegion(mapView.regionThatFits(region), animated: true)
        }
    }
    

    Then I placed my MapView in my ContentView, passing a location argument, which I will explain next:

    ContentView:

    struct ContentView: View {
    
        @ObservedObject var viewModel: ContentViewModel
    
        var body: some View {
            VStack {
                MapView(location: self.$viewModel.location)
            }
        }
    }
    

    In my ViewModel, I handle location changes using a delegate, here is the code with more details in comments:

    class ContentViewModel: ObservableObject {
        //location is a Published value, so the view is updated every time location changes
        @Published var location: CLLocation = CLLocation.init()
    
        //LocationWorker will take care of CLLocationManager...
        let locationWorker: LocationWorker = LocationWorker()
    
        init() {
            locationWorker.delegate = self
        }
    
    }
    
    extension ContentViewModel: LocationWorkerDelegate {
        func locationChanged(lastLocation: CLLocation?) {
            //Location changed, I change the value of self.location, it is a @Published value so it will refresh the @Binding variable inside MapView and call MapView.updateUIView
            self.location = CLLocation.init(latitude: lastLocation!.coordinate.latitude, longitude: lastLocation!.coordinate.latitude)
        }
    }
    

    And finally here is LocationWorker which take cares of CLLocationManager():

    class LocationWorker: NSObject, ObservableObject  {
    
        private let locationManager = CLLocationManager()
        var delegate: LocationWorkerDelegate?
    
        let objectWillChange = PassthroughSubject<Void, Never>()
    
        @Published var locationStatus: CLAuthorizationStatus? {
            willSet {
                objectWillChange.send()
            }
        }
    
        @Published var lastLocation: CLLocation? {
            willSet {
                objectWillChange.send()
            }
        }
    
        override init() {
            super.init()
            self.locationManager.delegate = self
            //...
        }
    }
    
    protocol LocationWorkerDelegate {
        func locationChanged(lastLocation: CLLocation?)
    }
    
    extension LocationWorker: CLLocationManagerDelegate {
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            guard let location = locations.last else { return }
            self.lastLocation = location
            //When location changes: I use my delegate ->
            if delegate != nil {
                delegate!.locationChanged(lastLocation: lastLocation)
            }
        }
    }