[Swift MapKit Tutorial Series] How To Customize the Map Annotations Callout, Request a Transit ETA and Launch the Transit Directions to your Destination

iOS 9 SDK has brought some cool new features to MapKit that worth to try out and use to enrich your map project. One of them is the Maps Transit feature and the ability to request a Transit ETA (Estimated Time of Arrival) as well as launching the Transit directions.

Let’s assume you are in London and you want to reach some nearby restaurants. iOS 9 now can calculate the estimated Transit time needed to get to the restaurant. Besides, it allows you to open the iOS maps app with a provided Transit directions.

Here is a quick demo of what you gonna do in this tutorial.

Demo - How To Customize the map annotations Callout, Request for the Transit ETA and Launch the Transit Itinerary to your destination

In addition to the Transit ETA and directions implementation, you will customise the pin view callout by taking advantage of the new MKAnnotationView‘s detailCalloutAccessoryView property.

Without further ado, let’s dive right in!

To begin with, download this images pack you will use on the project.

Open up Xcode, select “File\New\Project” from the menu, choose the Single View Application template and name the project “Restaurants Transit”.

Create new Xcode project

Setting up the UI

The user interface is pretty simple, a map view and a button to move from one restaurant to the next one.

Select the Main.storyboard file from the Project navigator view, then drag a UIToolbar from the Object library and place it at the bottom of the view.

Now let’s apply some Auto Layout constraints to the tool bar in order to keep it at the bottom of the screen.

Make sure the tool bar is selected, then select the Pin menu and set the leading, bottom and trailing constraints to 0, make sure to Uncheck the Constrain to margins checkbox, then hit the Add Constraints button to apply.

Set the leading, bottom and trailing Auto Layout constraints to the UIToolbar

Click to enlarge

The tool bar came with a default bar button item, let’s change its default text. Select it from the Document Outline view then switch to the Attributes inspector and change the Title property value to “Next Restaurant”.

Change the UIBarButtonItem Title

That was easy 🙂

Another remaining easy step is to set up the map object in the view. To do so, drag an MKMapView object from the Object library and adjust it manually in a way to fill all the rest of the view above the tool bar. Then, set the (leading, bottom, trailing and top) Auto Layout constraints for the map to 0 from the Pin menu, so that it will always stick to the screen margins.

Place the MKMapView object with Auto Layout

To finish up with the UI, you need to hook up an IBOutlet reference for the map view to the code as well as an IBAction for the button.

Let’s quickly do that, switch to the Assistant editor and make sure the ViewController.swift file is loaded in the split editor along with the storyboard. Now, ctrl click on the map view and drag a line to the code, set the outlet name to “mapView” and hit the “Connect” button. Next, ctrl click on the tool bar button and drag a line to the code, this time, set the Connection type to “Action” and name it “showNext”.

Here is a quick animation for more clarity.

Hook up map view and tool bar button to the code


Yes, we are done with the UI, don’t forget to import all the images you downloaded previously to the project (drag all images to the Project navigator view and make sure the “Copy items if needed” checkbox is checked).

Final Project navigator view

Time for code now 🙂

First thing to do is to import the MapKit framework. Select the ViewController.swift file from the Project navigator view and add the following import statement at the top of the class:

Next, let’s add some properties. Copy the following declarations right after the class name:

The four array properties will store the relevant data for the restaurants (names, images, descriptions and coordinates). As for the currentRestaurantIndex property, it is important to keep track of the current displayed restaurant and hence to get the right infos for the next restaurant to locate in the map.

Let’s fill the arrays you declared above with some data. Locate the viewDidLoad method and implement the following code inside (right after the super.viewDidLoad() call):

Usually you want to calculate the Transit ETA between your current location and a destination point (the restaurant in this case). Hence, let’s locate the user current location in the map.

Always inside the viewDidLoad, copy the following code at the end of the method:

Before accessing the user’s location information, you need to ask for user permission. Once user grants permission, the location manager will be able to get the current location.

Also, you need to add the NSLocationWhenInUseUsageDescription key to the Info.plist file. Remember, this key specifies the reason for accessing the user’s location information and is supported since iOS 8.

Select the Info.plist file from the Project navigator view.

The Info.plist in the Project navigator view

Then add the NSLocationWhenInUseUsageDescription with some message to show along with the alert, something like this:

Add the NSLocationWhenInUseUsageDescription Key to the Info.plist file

Before you run the app, change the class declaration like below, so that it conforms to the MKMapViewDelegate protocol (since the current class is the delegate of the map view).

Also, you need a way to move and center the visible region on the map around the user current location each time it gets changed. Seems like the mapView:didUpdateUserLocation: protocol method is the best place to do so. Go ahead and copy the following code before the closing bracket of the class:

So far so good, run the app to check everything is going right. You should get the permission alert like below:

NSLocationWhenInUseUsageDescription, location, permission alert

Click the “Allow” button and change the simulated location to “London, England” from the Location pop-up menu at the top of the Debug area:

Change the simulated location from the Debug area

You will notice the app moves to the selected region in London! Good job so far!

MKMapView current user location

Placing The Restaurants On The Map

Now as the current location is tracked, you gonna show some nearby restaurants in the London area. The code will be implemented in the action method so that it runs each time you press the tool bar button to move to the next restaurant in the list.

Locate the showNext action method and place the following code inside:

Let’s breakdown the code above to get a deep understanding on how it goes:

// 1: This is a simple test as you click the tool bar button to iterate over the array items. If you reach the last restaurant in the list, it will be reset to 0 to show the first item and goes from there.

// 2: Here you used the currentRestaurantIndex value to get the restaurant coordinates, name and image informations. Then, you constructed a custom annotation object to store those informations for later use in the mapView:viewForAnnotation: protocol method.

// 3: Here is the main part; In order to calculate the Transit ETA (Estimated Time of Arrival) for each restaurant, you need two locations (source and destination). The source point is the user current location coordinates, while the destination is the restaurant we want to reach. After preparing those informations and initializing an MKDirectionsRequest object, it’s time to launch the ETA request. To do this, you invoked the calculateETAWithCompletionHandler API on the directions object.

The calculateETAWithCompletionHandler API runs asynchronously, and its completion handler will run on the main thread, this is where you check for the returned results. If no error occurred, you converted the ETA estimated time to minutes (since it’s returned in seconds) and assign it to the custom annotation eta property.

// 4: This is to check whether the restaurant pin to show was already shown before on the map view. If it’s new annotation, then add it, if not, then reuse the current annotation in the map view annotations array.

The selectAnnotation API will programmatically select the specified annotation and display a callout view for it.

Once a pin is selected, a callout view needs some informations to display. Since you decided to customise the callout, you will need to implement one more MKMapViewDelegate protocol method to take care of the view customization.

Before the closing bracket of the class, implement the following code:

Since the callout customization is only restricted to “RestaurantAnnotation” instances, you need to skip all other types of annotations (like MKUserLocation types). In such cases, you just returned nil as you did above.

The rest of the code is just for setting up the layout of the callout view. Note the detailCalloutAccessoryView property, introduced in iOS 9 which now allows to natively add custom content to the callout view, such us images and long text views.

Before you run the app, let’s add the famous RestaurantAnnotation class to the project.

Select “File\New\File” from the menu, choose the “Cocoa Touch Class” file type and name it RestaurantAnnotation. Make sure it’s an NSObject subclass.

Add an NSObject subclass to Xcode

Next, select the RestaurantAnnotation.swift file from the Project navigator view and change the class content to the following:


Alright, run the app and make sure to point your simulated location to “London, England”. Then click the tool bar button to start showing up the restaurants!

Show restaurants pin on the map - Demo

You are almost done, the view callout has all the informations you set up earlier in the viewForAnnotation protocol method. One last thing to do is to call the iOS maps app in order to launch the Transit directions to the restaurant.

To do so, select the ViewController.swift file to load it in the editor, and implement the following protocol method before the closing bracket of the class:

The above method is also an MKMapViewDelegate protocol method, it runs each time you click one of the annotation view’s accessory buttons (in our case the bus button on the right accessory view). This is the correct place and time to launch the iOS maps app to draw the Transit directions to your destination.

Also note that specifying the MKLaunchOptionsDirectionsModeKey option in the launchOptions dictionary above will make the Maps app interprets that as an attempt to map from the user’s current location to the location specified by the map item constant (which is the restaurant location). So in such context, the maps app is clever enough and doesn’t need further specifications.

That’s it, run the app, browse some of Londons’ restaurants and click the bus button on the right of the callout view to get you some nice Transit directions 🙂

As usual, you can download the final project here.

Feel free to leave a comment below and tell me whether you consider supporting Transit directions on your next maps app? I’d love to hear from you!

About Malek

Malek is a passionate iOS Engineer and Founder of Medigarage Studios, a small mobile games startup. I started my iOS adventures in 2011 and since then I fell in love with it. You can hire me for your project, get in touch by Email to discuss further details. Also, feel free to reach out on Twitter and Google+.

  • Michael Brey

    Thank you for presenting your superb solution to customizing annotation callouts!
    One of the many reasons I am so pleased about your tutorial is because it works with the newest features of IOS9. It is not making things complicated and awkward by using XIB files or .h / .m classes like other tutorials/code templates do. I encourage everyone who is interested in developing custom callouts to study your tutorial!

  • Ivan Alvarado

    Great tutorial! I am trying to implement the part where you calculate the ETA to a restaurant from the user’s current location, in my code. I have placed the ETA algorithm of the tutorial in a function called ‘calculateETA() -> Object’ which returns an object. However, when I call calculateETA(), the


    line is being bypassed the first time the program encounters the line, and the function returns, then the line gets executed after the function has returned. This is really funky behavior and I have verified this with ‘print()’ statements throughout my code and calculateETA(). I assume I don’t fully understand how ‘calculateETAWithCompletion’ works, so I was reading Apple’s documentation on the function and there’s a function ‘calculating’ which returns ‘true’ if an MKDirections process is still executing. So, I’ve placed a print statement right before calculateETAWithCompletionHandler that prints whether MKDirections is still calculating the ETA, but it always returns ‘false’.

    Any help is greatly appreciated.

  • Malek_T

    Thanks Michael, I am glad to help 🙂

  • Malek_T

    Hi Ivan 🙂
    You are getting the obvious behavior. Since calculateETAWithCompletion is an asynchronous API, it will run on the background and only executes its completion handler from the app’s main thread. That explains why the function you wrote (calculateETA) returns before the calculateETAWithCompletion finish its work.
    Solution: Define your calculateETA function with a completion callback parameter then – inside the calculateETAWithCompletion completion handler – call the callback you just defined as parameter (in calculateETA).

    Happy coding!

  • Ivan Alvarado

    Thank you for you fast reply! I was able to get it to work by returning in the asynchronous method using a closure. I posted on stackoverflow here: http://stackoverflow.com/questions/35076423/mkdirections-calculateetawithcompletionhandler-executing-with-delay-swift-2-0

  • Malek_T

    Glad you make it!

  • Steve

    When I try to run it to my Ipad or Iphone i get the error Ambiguous use of subscript. (let latitude: Double = coordianate [0] as! Double)

  • Malek_T

    Hi Steve, which Xcode version you are using?

  • Steve

    Version 7.3

  • Khalid Alkhatib

    How can I show all of the restaurants at once?

  • Malek_T

    Hi Khaled, I posted the solution in the Forum, here is the link 🙂

  • Malek_T

    Hi Steve, please use the following (I will update the tutorial for iOS 10):
    let coordinate:[Double] = coordinates[currentRestaurantIndex] as! [Double]
    let latitude: Double = coordinate[0]