Paolo Marini
Paolo Marini

Reputation: 323

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.

Upvotes: 4

Views: 15574

Answers (2)

zgluis
zgluis

Reputation: 3340

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

Upvotes: 6

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 119108

Instead of calling a View method directly from outside, you should revise your logic a bit and just change some kind of a state somewhere and let the View update itself. Take a look at this algorithm:


The classic (and worst) way:

  1. Location changed
  2. Delegate method called in the app delegate (Better refactor to else where)
  3. App delegate calls a method directly on the view (You should pass a reference to that view all the way up to the app delegate)

Although the above algorithm is what you are looking for originally, It isn't the best way and I don't recommend it at all! But it will work 🤷🏻‍♂️


The SwiftUI way:

  1. Location changed
  2. Delegate method called in the responsible object (maybe a singleton location location manager instance 🤷🏻‍♂️)
  3. Location manager updates a State somewhere. (maybe an ObservedObject variable inside itself or an EnvironmentObject or etc.)
  4. All views that subscribed for changes of that property will notify about the changes
  5. All notified views will update themselves.

This is how it should be done. But there are more than just one way to implement this and you should consider your preferences to pick the best for you.

Upvotes: 0

Related Questions