MKDirections Tutorial (2022)

iOS 7 has given app developers the ability to add directions to their apps thanks to a new class in the Map Kit Framework called MKDirections. In this tutorial we will take a look at this class and create a simple app that lets you search for an address and calculate the distance as well as receive route-based directions from your current location. This tutorial will work on either an iOS device or on the simulator.

The MKDirections Class

The MKDirections class is relatively small and just has 4 methods and 1 property. The property is called calculating and shows you if direction calculation is currently in progress. The instance methods are as follows:

Initializing a Directions Object
€“ initWithRequest:
Getting the Directions
€“ calculateDirectionsWithCompletionHandler:
€“ calculateETAWithCompletionHandler:
€“ cancel

The initWithRequest: method is used first and accepts an MKDirectionsRequest as an argument. This is used to provide start and end points of the route. As well as specifying start and end points the MKDirectionsRequest also can be configured to set the transportType (through a property) as well as request alternate routes and finally specify travel dates to get a more accurate ETA. An example of specifying travel dates would be the Apple servers recognising you are travelling in rush hour traffic and then providing alternative routes that are less congested.

When you have initialised the object, the next step is to make the request for directions. This is done with either the calculateDirectionsWithCompletionHandler method or calculateETAWithCompletionHandler method.

The latter of the two methods (calculateETAWithCompletionHandler:) is the quicker of the two methods to call and if you only need estimates of travel time then use this rather than the full directions method.

The MKRoute Class

When a route has been calculated you are provided an NSArray by the completion handler. This NSArray contains MKRoute objects (one for each route). Looking in to those you get route geometry in the form of polyline (an MKPolyline object) as well as steps which is the list of directions provided to get you from A to B. Other properties included in this class are name, advisoryNotices, distance, expectedTravelTime and transportType. Some of those branch off in to other objects.

As can be seen, MKDirections is quite an extensive and useful class. Rather than having to switch to Apple Maps like you did previously, you can now put the directions in your map as well as provide ETA and other information without the user having to leave your app.

MKDirections Tutorial

In this tutorial we are going to create an app that shows a map in the view and allows you search for an address. When searched, an annotation (pin) will drop. Directions will then be calculated as well as other information about the trip. We’ll also have the ability to look at the directions as well as put an overlay on the map.

Lets begin by creating a single view application for the iPhone.

MKDirections Tutorial (1)

Add the Core Location and Map Kit Frameworks to the project as pictured above.

MKDirections Tutorial (2)

Next, lets add a UIToolbar to the bottom of the view. We next need to add an MKMapView to fill the top half of the screen. Add the labels and UITextView as well as the buttons on the toolbar.

MKDirections Tutorial (3)Now activate the assistant editor (centre icon at the top right of the screen) and CTRL+drag from the MKMapView to the ViewController.h file. Call this mapView. At this point, we can also import the two frameworks that are needed for this project.

Your ViewController.h should now contain the following:

#import #import #import @interface ViewController : UIViewController@property (weak, nonatomic) IBOutlet MKMapView *mapView;@end

Now that we have a basic view to work with, lets start creating the code that will allow the app to work.

MKDirections Tutorial (4)We’ll tackle the current location first and use that to set the setSource property in the MKDirectionsRequest. Rather than use the CLLocation property in Core Location, we’ll use the MKUserLocation class and access the location property within that. Lets first enable the “shows user location” behaviour on the MKMapView. We’ll do that in the inspector (see image to the left). To access that, load up the storyboard and then click the MKMapView and select the attributes inspector link and then check the check box.

We could store the user location found at self.mapView.userLocation.location, but instead, we’ll just use that code when we make the request and call it rather than store and then call it.

Now that we have a start point for our journey (our current location), we are now going to create the end point. To do this I thought it would work well to have a search box and then search by typing in an address and then doing a forward geocode. I wrote about this in the CLGeocode tutorial a couple of months ago.

MKDirections Tutorial (5)Lets begin by overlaying a UITextField at the top of the screen (on top of the map). We then need to CTRL+drag from the UITextField to ViewController.m file. When prompted, select the “editing did end” option and also change id to UITextField and then hit connect.

We now need to add the code to do a forward-geocode, that is… take the address you entered and convert it to some coordinates.

- (IBAction)addressSearch:(UITextField *)sender { CLGeocoder *geocoder = [[CLGeocoder alloc] init]; [geocoder geocodeAddressString:sender.text completionHandler:^(NSArray *placemarks, NSError *error) { if (error) { NSLog(@"%@", error); } else { thePlacemark = [placemarks lastObject]; float spanX = 1.00725; float spanY = 1.00725; MKCoordinateRegion region; region.center.latitude = thePlacemark.location.coordinate.latitude; region.center.longitude = thePlacemark.location.coordinate.longitude; region.span = MKCoordinateSpanMake(spanX, spanY); [self.mapView setRegion:region animated:YES]; [self addAnnotation:thePlacemark]; } }];}

On line 2 we alloc/init the CLGeocoder object.

We then call the geocodeAddressString method and provide it with the text of the sender (the text you you will type in to the search box).

Next we check for errors and if no errors, we assign the lastObject from the placemarks array provided by the completion handler to thePlacemark (line 7) which is a CLPlacemark instance variable created at the top of the code. Next we set a span for X and Y and then create an MKCoordinateRegion. We finally zoom to that region.

To make it clear where we are navigating to, I added a call to the addAnnotation custom method and provided the placemark with it.

We now need to declare the CLPlacemark to get rid of any errors. Add the following just below @implementation ViewController in the implementation:

CLPlacemark *thePlacemark;

Lets now create the addAnnotation: method.

- (void)addAnnotation:(CLPlacemark *)placemark { MKPointAnnotation *point = [[MKPointAnnotation alloc] init]; point.coordinate = CLLocationCoordinate2DMake(placemark.location.coordinate.latitude, placemark.location.coordinate.longitude); point.title = [placemark.addressDictionary objectForKey:@"Street"]; point.subtitle = [placemark.addressDictionary objectForKey:@"City"]; [self.mapView addAnnotation:point];}

To show the annotation at the coordinate provided by the forward-geocode, we first alloc/init an MKPointAnnotation. We then set the coordinate from placemark.location.coordinate.latitude (longitude as well) so that the annotation lands on the correct location.

What we do next is access the addressDictionary property of placemark and from that, we pull out the Street key and City key which provides some details when you tap on the pin. We assign these to point.title and point.subtitle.

Finally, we add the annotation to the mapView.

Tapping on the pin should reveal the address of where the pin is. Please note that if your search doesn’t provide any placemark info other than coordinates then the callout will not pop up. To prevent this we would error check but I’ll leave that to you to add if you so wish. Search for something more specific if this happens (if you opt not to error check yourself).

To get directions and other information, what we’ll do now is add a right callout that we can tap on to request the directions.

To do this, we need to add the MKMapViewDelegate protocol to the header as follows:

@interface ViewController : UIViewController 

We then need to override a method from the delegate called mapView:viewForAnnotation:. I’ll briefly cover this method here, but for a better example, check out my MKPointAnnotation tutorial.

The method is fairly boilerplate other than a few tweaks.

-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation { // If it's the user location, just return nil. if ([annotation isKindOfClass:[MKUserLocation class]]) return nil; // Handle any custom annotations. if ([annotation isKindOfClass:[MKPointAnnotation class]]) { // Try to dequeue an existing pin view first. MKPinAnnotationView *pinView = (MKPinAnnotationView*)[self.mapView dequeueReusableAnnotationViewWithIdentifier:@"CustomPinAnnotationView"]; if (!pinView) { // If an existing pin view was not available, create one. pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"CustomPinAnnotationView"]; pinView.canShowCallout = YES; } else { pinView.annotation = annotation; } return pinView; } return nil;}

Line 3 we are checking if the annotation is an MKUserLocation. If so, we don’t do anything other than return nil. This check makes the blue ball stay as it is. If we didn’t have this line then we would make changes to this AnnotationView as well.

Next, we look for an MKPointAnnotation. As we added an MKPointAnnotation to the map, this is true and the code runs on line 7 onwards.

Line 8 created an MKPinAnnotationView and looks for any annotations that can be reused (perhaps they have scrolled out of view).

Now that the annotation drops we can start looking at getting the route. We already know our current location and our destination which means we are ready to search for directions.

Before testing the current code, add the following method to viewDidLoad or the viewForAnnotation delegate method will not be called.

self.mapView.delegate = self;

Implementing MKDirections and MKDirectionsRequest

Now that our “app” is capable of searching for an address and providing an annotation that shows the found location on a map, we are now ready to get the directions. The way I have done this (quite sloppily if you ask me… but still shows what you need to know to get it working) is use a button on the toolbar and CTRL+dragged to the implementation to create an IBAction. Tapping this button will start the directions request, look for the results and then put the overlay on the map.

I called the method routeButtonPressed:. There’s quite a lot going on here which should probably be pushed in to a custom object so it can be reused elsewhere. But, for the sake of this small project we’ll just add it in the ViewController.m.

- (IBAction)routeButtonPressed:(UIBarButtonItem *)sender { MKDirectionsRequest *directionsRequest = [[MKDirectionsRequest alloc] init]; MKPlacemark *placemark = [[MKPlacemark alloc] initWithPlacemark:thePlacemark]; [directionsRequest setSource:[MKMapItem mapItemForCurrentLocation]]; [directionsRequest setDestination:[[MKMapItem alloc] initWithPlacemark:placemark]]; directionsRequest.transportType = MKDirectionsTransportTypeAutomobile; MKDirections *directions = [[MKDirections alloc] initWithRequest:directionsRequest]; [directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) { if (error) { NSLog(@"Error %@", error.description); } else { routeDetails = response.routes.lastObject; [self.mapView addOverlay:routeDetails.polyline]; self.destinationLabel.text = [placemark.addressDictionary objectForKey:@"Street"]; self.distanceLabel.text = [NSString stringWithFormat:@"%0.1f Miles", routeDetails.distance/1609.344]; self.transportLabel.text = [NSString stringWithFormat:@"%u" ,routeDetails.transportType]; self.allSteps = @""; for (int i = 0; i < routeDetails.steps.count; i++) { MKRouteStep *step = [routeDetails.steps objectAtIndex:i]; NSString *newStep = step.instructions; self.allSteps = [self.allSteps stringByAppendingString:newStep]; self.allSteps = [self.allSteps stringByAppendingString:@"\n\n"]; self.steps.text = self.allSteps; } } }];}

Note: Don't worry about an error at the moment. It will be fixed a few steps down.

On line 2 we create an MKDirectionsRequest and alloc/init it. Remember that this is what is needed to make an MKDirections search.

On line 3 we create an MKPlaceMark and initialise it with an instance variable declared at the top of the code (the CLPlacemark).

Lines 4 and 5 we are setting the source and destination. The source is an MKMapItem and we can use the mapItemForCurrentLocation method here. For destination, we initialise with the placemark created earlier.

We then set the transport type on line 6 and on line 7 we alloc/init an MKDirections object with the directionsRequest (MKDirectionsRequest) object.

On line 8 we call the calculateDirectionsWithCompletionHandler on MKDirections and then on lines 9 - 25 we run the code to handle the MKDirectionsResponse which contains all the information we need to show a route and get routing details.

On line 12 we assign routeDetails with the last object of the response.routes property. To fix that error we need to paste in the following line just below the implementation:

MKRoute *routeDetails;

Now that we have an MKRoute object containing the route details from start to end, we can start querying the properties and displaying information on the screen.

To do that we need to step away from this section of code to the storyboard and link up all of our labels and UITextView. This is how I connected them to the header:

@property (weak, nonatomic) IBOutlet UILabel *destinationLabel;@property (weak, nonatomic) IBOutlet UILabel *distanceLabel;@property (weak, nonatomic) IBOutlet UILabel *transportLabel;@property (weak, nonatomic) IBOutlet UITextView *steps;@property (strong, nonatomic) NSString *allSteps;

We have 3 IBOutlets for a UILabel and one IBOutlet for a UITextView. I opted for a UITextView because we need to show multiple lines of text for the directions.

The NSString will be used a little later as well (to form the directions for the UITextView).

Moving on to line 13, we now need to add the route overlay to the MKMapView. We find that information in the polyline property of our routeDetails. At this point, the overlay will not be added as we need to configure the overlay in a delegate method which we will get to shortly.

On lines 14 - 16 we assign information to the labels' text properties. For the distance label on line 15 we specify 1 decimal place and add Miles on to the end. We then call in routeDetails.distance and divide that by how many meters per mile there are. For line 16, I would suggest converting that code in to a human readable format by using a switch statement. The typedef below shows you what numbers translate in to what mode of transport.

typedef enum { MKDirectionsTransportTypeAutomobile = 1 << 0, MKDirectionsTransportTypeWalking = 1 << 1, MKDirectionsTransportTypeAny = NSUIntegerMax} MKDirectionsTransportType;

On line 17 we are assigning an empty string to self.allSteps.

Lines 18 - 24 we iterate with a for loop and look at the route details for each step. We then pull the step instructions (such as Turn Right in 50 meters) and use the stringByAppendingString class method and assign the response back to self.allSteps. We then repeat again but add a couple of line breaks. We then add the text to the text label. This could actually be done out of the loop, but either way it isn't too expensive to do it for all the duration of the loop.

The next step is to add the delegate method that configures the overlay. We just need to implement this short method which is also fairly boilerplate for this project:

-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id)overlay { MKPolylineRenderer * routeLineRenderer = [[MKPolylineRenderer alloc] initWithPolyline:routeDetails.polyline]; routeLineRenderer.strokeColor = [UIColor redColor]; routeLineRenderer.lineWidth = 5; return routeLineRenderer;}

We alloc/init an MKPolylineRender object and init it with the polyline property of routeDetails. We then define the strokeColor and the width of the line and then we return the MKPolylineRenderer object. This simply tells the app what type of overlay is needed and what it will look like.

The last step is to connect up the clear button IBAction and use the following code to just clear all the labels and the route:

- (IBAction)clearRoute:(UIBarButtonItem *)sender { self.destinationLabel.text = nil; self.distanceLabel.text = nil; self.transportLabel.text = nil; self.steps.text = nil; [self.mapView removeOverlay:routeDetails.polyline];}

UPDATE: Location Fix

The user of the app needs to also give permission. In earlier versions of iOS, MapKit either didn't require this or took care of it. To get around this problem of not being able to get directions you need to make the following changes:

In your .plist file you need to add a new key and description. Right click and add a new row. Paste the following in the left hand side:

NSLocationWhenInUseUsageDescription

In the right hand side it will be set to a string. Here you would put a description that shows up on the screen when the device prompts the user to allow location monitoring.

Switch back to the ViewController.m file and add the following:

#import "ViewController.h"#import <CoreLocation/CoreLocation.h>@interface ViewController ()@property (strong, nonatomic) CLLocationManager *locationManager;@end@implementation ViewController

The lines we are adding here are the import for CoreLocation as well as the CLLocationManager property.

In viewDidLoad add the following:

if (!self.locationManager) { self.locationManager = [[CLLocationManager alloc] init];}[self.locationManager requestWhenInUseAuthorization];

When you launch the app you will not be prompted to allow your current location to be accessed. By doing this you can now get directions from your current location to a selected location.

Testing the MKDirections Test App

Now that you are ready for testing, we need to make sure your device or simulator is configured correctly. As we included very little error checking, we need to make sure that our app doesn't error out.

So, when prompted you need to accept that the device can use your current location. If you see a blue dot on the map then you are ready to type in an address to search for. Search for it and then click Get Route. You should then see a red line of the route on the map along with all the direction steps as well as distance you will travel to get there.

On the simulator you might need to enable location by clicking Debug > Location > Apple and then waiting for a prompt or blue ball to appear.

MKDirections Tutorial (6)

As usual, you can download the full project here and also see the code below:

ViewController.h

#import #import #import @interface ViewController : UIViewController @property (weak, nonatomic) IBOutlet MKMapView *mapView;@property (weak, nonatomic) IBOutlet UILabel *destinationLabel;@property (weak, nonatomic) IBOutlet UILabel *distanceLabel;@property (weak, nonatomic) IBOutlet UILabel *transportLabel;@property (weak, nonatomic) IBOutlet UITextView *steps;@property (strong, nonatomic) NSString *allSteps;@end

ViewController.m

#import "ViewController.h"#import @interface ViewController ()@property (strong, nonatomic) CLLocationManager *locationManager;@end@implementation ViewControllerCLPlacemark *thePlacemark;MKRoute *routeDetails;- (void)viewDidLoad{ [super viewDidLoad];// Do any additional setup after loading the view, typically from a nib. self.mapView.delegate = self; if (!self.locationManager) { self.locationManager = [[CLLocationManager alloc] init]; } [self.locationManager requestWhenInUseAuthorization];}- (IBAction)routeButtonPressed:(UIBarButtonItem *)sender { MKDirectionsRequest *directionsRequest = [[MKDirectionsRequest alloc] init]; MKPlacemark *placemark = [[MKPlacemark alloc] initWithPlacemark:thePlacemark]; [directionsRequest setSource:[MKMapItem mapItemForCurrentLocation]]; [directionsRequest setDestination:[[MKMapItem alloc] initWithPlacemark:placemark]]; directionsRequest.transportType = MKDirectionsTransportTypeAutomobile; MKDirections *directions = [[MKDirections alloc] initWithRequest:directionsRequest]; [directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) { if (error) { NSLog(@"Error %@", error.description); } else { routeDetails = response.routes.lastObject; [self.mapView addOverlay:routeDetails.polyline]; self.destinationLabel.text = [placemark.addressDictionary objectForKey:@"Street"]; self.distanceLabel.text = [NSString stringWithFormat:@"%0.1f Miles", routeDetails.distance/1609.344]; self.transportLabel.text = [NSString stringWithFormat:@"%lu" ,(unsigned long)routeDetails.transportType]; self.allSteps = @""; for (int i = 0; i < routeDetails.steps.count; i++) { MKRouteStep *step = [routeDetails.steps objectAtIndex:i]; NSString *newStep = step.instructions; self.allSteps = [self.allSteps stringByAppendingString:newStep]; self.allSteps = [self.allSteps stringByAppendingString:@"\n\n"]; self.steps.text = self.allSteps; } } }];}- (IBAction)clearRoute:(UIBarButtonItem *)sender { self.destinationLabel.text = nil; self.distanceLabel.text = nil; self.transportLabel.text = nil; self.steps.text = nil; [self.mapView removeOverlay:routeDetails.polyline];}- (IBAction)addressSearch:(UITextField *)sender { CLGeocoder *geocoder = [[CLGeocoder alloc] init]; [geocoder geocodeAddressString:sender.text completionHandler:^(NSArray *placemarks, NSError *error) { if (error) { NSLog(@"%@", error); } else { thePlacemark = [placemarks lastObject]; float spanX = 1.00725; float spanY = 1.00725; MKCoordinateRegion region; region.center.latitude = thePlacemark.location.coordinate.latitude; region.center.longitude = thePlacemark.location.coordinate.longitude; region.span = MKCoordinateSpanMake(spanX, spanY); [self.mapView setRegion:region animated:YES]; [self addAnnotation:thePlacemark]; } }];}- (void)addAnnotation:(CLPlacemark *)placemark { MKPointAnnotation *point = [[MKPointAnnotation alloc] init]; point.coordinate = CLLocationCoordinate2DMake(placemark.location.coordinate.latitude, placemark.location.coordinate.longitude); point.title = [placemark.addressDictionary objectForKey:@"Street"]; point.subtitle = [placemark.addressDictionary objectForKey:@"City"]; [self.mapView addAnnotation:point];}-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id)overlay { MKPolylineRenderer * routeLineRenderer = [[MKPolylineRenderer alloc] initWithPolyline:routeDetails.polyline]; routeLineRenderer.strokeColor = [UIColor redColor]; routeLineRenderer.lineWidth = 5; return routeLineRenderer;}-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation { // If it's the user location, just return nil. if ([annotation isKindOfClass:[MKUserLocation class]]) return nil; // Handle any custom annotations. if ([annotation isKindOfClass:[MKPointAnnotation class]]) { // Try to dequeue an existing pin view first. MKPinAnnotationView *pinView = (MKPinAnnotationView*)[self.mapView dequeueReusableAnnotationViewWithIdentifier:@"CustomPinAnnotationView"]; if (!pinView) { // If an existing pin view was not available, create one. pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"CustomPinAnnotationView"]; pinView.canShowCallout = YES; } else { pinView.annotation = annotation; } return pinView; } return nil;}- (void)didReceiveMemoryWarning{ [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated.}@end

Top Articles

You might also like

Latest Posts

Article information

Author: Horacio Brakus JD

Last Updated: 10/23/2022

Views: 6623

Rating: 4 / 5 (71 voted)

Reviews: 86% of readers found this page helpful

Author information

Name: Horacio Brakus JD

Birthday: 1999-08-21

Address: Apt. 524 43384 Minnie Prairie, South Edda, MA 62804

Phone: +5931039998219

Job: Sales Strategist

Hobby: Sculling, Kitesurfing, Orienteering, Painting, Computer programming, Creative writing, Scuba diving

Introduction: My name is Horacio Brakus JD, I am a lively, splendid, jolly, vivacious, vast, cheerful, agreeable person who loves writing and wants to share my knowledge and understanding with you.