Directions to annotation from current location in MapKit

Hello, Im trying to implement directions (navigation) from my app. All I want to do is to open map app (Google Maps or Apple Maps) with ready to go directions after user click button - in my case leftCalloutAccessoryView called leftButton. I've been trying some options, but without a good result. Here is my code:

    func findPlace(_ places: [Place]) {

      for place in places {

        let annotations = MKPointAnnotation()

        annotations.title = place.name

        annotations.subtitle = place.description

        annotations.coordinate = CLLocationCoordinate2D(latitude:

          place.lattitude, longitude: place.longtitude)

        mapView.addAnnotation(annotations)

      }

    }



    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {

        guard !(annotation is MKUserLocation) else { return nil }

        mapView.delegate = self

        let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: String(annotation.hash))

        let identifier = "identifier"

        annotationView.canShowCallout = true

        guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: annotation) as? MKMarkerAnnotationView else { return nil }

        let rightButton = UIButton(type: .detailDisclosure)

        rightButton.tag = annotation.hash

        annotationView.canShowCallout = true

        annotationView.rightCalloutAccessoryView = rightButton

        

        let leftButton = UIButton(frame: CGRect(

          origin: CGPoint.zero,

          size: CGSize(width: 25, height: 25)))

        leftButton.setBackgroundImage(#imageLiteral(resourceName: "nav"), for: .normal)

        annotationView.leftCalloutAccessoryView = leftButton

        leftButton.addTarget(self, action: #selector(didClickDetailDisclosureNavigation(button:)), for: .touchUpInside)

    @objc func didClickDetailDisclosureNavigation(button: UIButton) {

            let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]

             ????.openInMaps(launchOptions: launchOptions)
//Here I've tried some solutions like `annotations.openInMaps(launchOptions: launchOptions)`- but it does not make sense.
    }
Answered by OOPer in 693115022

I would like to do this like here: https://www.raywenderlich.com/7738344-mapkit-tutorial-getting-started

If you want to do things as shown in the tutorial, you need to instantiate a MKMapItem as instructed.

As you are using MKPointAnnotation, you can define an extension like this:

extension MKPointAnnotation {
    var mapItem: MKMapItem {
        let placemark = MKPlacemark(coordinate: self.coordinate)
        return MKMapItem(placemark: placemark)
    }
}

To utilized this extension, you may need to pass the right annotation (which needs to be an MKPointAnnotation) to didClickDetailDisclosureNavigation(button:).

Which may be hard using a usual UIButton. You should better define your own button type which can hold an annotation.

Not too complex:

class AnnotationButton: UIButton {
    var annotation: MKPointAnnotation?
}

(Please do not put the extension and the class definition inside some other type. They need to be toplevel definitions.)


Your code would become something like this:

    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        
        guard !(annotation is MKUserLocation) else { return nil }
        
        //mapView.delegate = self //->Move this to `viewDidLoad()`
        
        //This `annotationView` will be shadowed by the following `annotationView`, not used.
        //let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: String(annotation.hash))
        
        let identifier = "identifier"
        
        //annotationView.canShowCallout = true
        
        guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: annotation) as? MKMarkerAnnotationView else { return nil }
        
        //...
        
        let leftButton = AnnotationButton(frame: CGRect( //<-
            origin: .zero,
            size: CGSize(width: 25, height: 25)))
        leftButton.setBackgroundImage(#imageLiteral(resourceName: "nav"), for: .normal)
        annotationView.leftCalloutAccessoryView = leftButton
        leftButton.addTarget(self, action: #selector(didClickDetailDisclosureNavigation(button:)), for: .touchUpInside)
        //↓↓
        if let pointAnnotation = annotation as? MKPointAnnotation {
            leftButton.annotation = pointAnnotation
        }
        //...
        return annotationView
    }
    
    @objc func didClickDetailDisclosureNavigation(button: AnnotationButton) { //<-

        let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]

        //↓↓
        if let mapItem = button.annotation?.mapItem {
            mapItem.openInMaps(launchOptions: launchOptions)
        }
    }

I haven't tried it myself (as you are not showing many parts of your code), but please try.

You have already the map displayed in mapView.

Why do you need to open yet another map ?

Anyway, to open an app (e.g., Google Maps), you have first to authorise in plist, then you pass an URL like

  • https://www.google.fr/maps/@48.856912,2.2912735,16z

or with your additional infos :

  • https://www.google.fr/maps/place/Tour+Eiffel/@48.8317476,2.3692721,12.3z/data=!4m5!3m4!1s0x47e66e2964e34e2d:0x8ddca9ee380ef7e0!8m2!3d48.8583701!4d2.2944813

You call:

          var mapPlace = "https://www.google.fr/maps/@48.8317476,2.3692721,12.3z"
          guard var mapsUrl = URL(string: mapPlace) else { return }

          if UIApplication.shared.canOpenURL(mapsUrl) {
               UIApplication.shared.open(mapsUrl, options: [:])
          } else {
              // redirect to safari because the user has not authorised a maps app (not the case here)
               mapsUrl = URL(string: "http://www.google.fr/maps")!
              UIApplication.shared.open(mapsUrl, options: [:])
          }

See details here : https://stackoverflow.com/questions/48072650/how-to-programmatically-check-and-open-an-existing-app-in-swift-4

Your initial question seemed to be about opening the maps app. Which the code I posted should allow you to do.

Does it work ?

I understand you have further need to help user navigate. What do you want to provide exactly ?

  • Route planning inside the map ?

Then passing the additional info should be OK. To find what to pass exactly in HTTP header, do the test in Maps: select an itinerary to search and look at the url ; it is like this (here to go from Paris-Orly airport to Toulouse city):

https://www.google.fr/maps/dir/Paris-Orly,+Orly/Toulouse/@46.1636622,-0.0010695,7z/data=!3m1!4b1!4m14!4m13!1m5!1m1!1s0x47e675b1fa6a3b1d:0x9d78ded743db8422!2m2!1d2.3652422!2d48.7262321!1m5!1m1!1s0x12aebb6fec7552ff:0x406f69c2f411030!2m2!1d1.444209!2d43.604652!3e0

  • Something else ?
Accepted Answer

I would like to do this like here: https://www.raywenderlich.com/7738344-mapkit-tutorial-getting-started

If you want to do things as shown in the tutorial, you need to instantiate a MKMapItem as instructed.

As you are using MKPointAnnotation, you can define an extension like this:

extension MKPointAnnotation {
    var mapItem: MKMapItem {
        let placemark = MKPlacemark(coordinate: self.coordinate)
        return MKMapItem(placemark: placemark)
    }
}

To utilized this extension, you may need to pass the right annotation (which needs to be an MKPointAnnotation) to didClickDetailDisclosureNavigation(button:).

Which may be hard using a usual UIButton. You should better define your own button type which can hold an annotation.

Not too complex:

class AnnotationButton: UIButton {
    var annotation: MKPointAnnotation?
}

(Please do not put the extension and the class definition inside some other type. They need to be toplevel definitions.)


Your code would become something like this:

    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        
        guard !(annotation is MKUserLocation) else { return nil }
        
        //mapView.delegate = self //->Move this to `viewDidLoad()`
        
        //This `annotationView` will be shadowed by the following `annotationView`, not used.
        //let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: String(annotation.hash))
        
        let identifier = "identifier"
        
        //annotationView.canShowCallout = true
        
        guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: annotation) as? MKMarkerAnnotationView else { return nil }
        
        //...
        
        let leftButton = AnnotationButton(frame: CGRect( //<-
            origin: .zero,
            size: CGSize(width: 25, height: 25)))
        leftButton.setBackgroundImage(#imageLiteral(resourceName: "nav"), for: .normal)
        annotationView.leftCalloutAccessoryView = leftButton
        leftButton.addTarget(self, action: #selector(didClickDetailDisclosureNavigation(button:)), for: .touchUpInside)
        //↓↓
        if let pointAnnotation = annotation as? MKPointAnnotation {
            leftButton.annotation = pointAnnotation
        }
        //...
        return annotationView
    }
    
    @objc func didClickDetailDisclosureNavigation(button: AnnotationButton) { //<-

        let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]

        //↓↓
        if let mapItem = button.annotation?.mapItem {
            mapItem.openInMaps(launchOptions: launchOptions)
        }
    }

I haven't tried it myself (as you are not showing many parts of your code), but please try.

@OOPer I've got Value of type 'UIButton' has no member 'annotation' when I try to add:

if let pointAnnotation = annotation as? MKPointAnnotation {

                    leftButton.annotation = pointAnnotation

                }
Directions to annotation from current location in MapKit
 
 
Q