Using MKDirections to get iOS 17 Map Directions and Routes

This chapter will explore using the MapKit MKDirections class to obtain directions and route information from within an iOS app. Having covered the basics, the MapSample tutorial app will be extended to use these features to draw routes on a map view and display turn-by-turn driving directions.

An Overview of MKDirections

The MKDirections class was introduced into iOS as part of the iOS 7 SDK and generates directions from one geographical location to another. The start and destination points for the journey are passed to an instance of the MKDirections class in the form of MKMapItem objects contained within an MKDirections.Request instance. In addition to storing the start and end points, the MKDirections.Request class also provides several properties that may be used to configure the request, such as indicating whether alternate route suggestions are required and specifying whether the directions should be for driving or walking.

Once directions have been requested, the MKDirections class contacts Apple’s servers and awaits a response. Upon receiving a response, a completion handler is called and passed the response as an MKDirection.Response object. Depending on whether or not alternate routes were requested (and assuming directions were found for the route), this object will contain one or more MKRoute objects. Each MKRoute object contains the distance, expected travel time, advisory notes, and an MKPolyline object that can be used to draw the route on a map view. In addition, each MKRoute object contains an array of MKRouteStep objects, each containing information such as the text description of a turn-by-turn step in the route and the coordinates at which the step is to be performed. In addition, each MKRouteStep object contains a polyline object for that step and the estimated distance and travel time.

The following code fragment demonstrates an example implementation of a directions request between the user’s current location and a destination location represented by an MKMapItem object named destination:

let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = destination
request.requestsAlternateRoutes = false

let directions = MKDirections(request: request)

directions.calculate(completionHandler: {(response, error) in

    if error != nil {
        print("Error getting directions")
    } else {
        self.showRoute(response)
    }
})Code language: Swift (swift)

The resulting response can subsequently be used to draw the routes on a map view using the following code:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func showRoute(_ response: MKDirections.Response) {

    for route in response.routes {

        routeMap.add(route.polyline,
                level: MKOverlayLevel.aboveRoads)
        for step in route.steps {
            print(step.instructions)
        }
    }
}Code language: Swift (swift)

The above code iterates through the MKRoute objects in the response and adds the polyline for each route alternate as a layer on a map view. In this instance, the overlay is configured to appear above the road names on the map.

Although the layer is added to the map view in the above code, nothing will be drawn until the rendererFor overlay delegate method is implemented. This method creates an instance of the MKPolylineRenderer class and then sets properties such as the line color and width:

func mapView(_ mapView: MKMapView, rendererFor
        overlay: MKOverlay) -> MKOverlayRenderer {

    let renderer = MKPolylineRenderer(overlay: overlay)
    renderer.strokeColor = UIColor.blue
    renderer.lineWidth = 5.0
    return renderer
}Code language: Swift (swift)

Note that this method will only be called if the class it resides in is declared as the delegate for the map view object. For example:

routeMap.delegate = selfCode language: Swift (swift)

Finally, the turn-by-turn directions for each step in the route can be accessed as follows:

for step in route.steps {
    print(step.instructions)
}Code language: Swift (swift)

The above code outputs the text instructions for each step of the route. As previously discussed, additional information may be extracted from the MKRouteStep objects as required by the app.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

With the basics of directions and routes in iOS covered, the MapSample app can be extended to implement some of this theory.

Adding Directions and Routes to the MapSample App

The MapSample app will now be modified to include a Details button in the toolbar of the first scene. When selected, this button will display a table view listing all locations’ names and phone numbers matching the most recent local search operation. Selecting a location from the list will display another scene containing a map displaying the route from the user’s current location to the selected destination.

Adding the New Classes to the Project

Load the MapSample app project into Xcode and add a new class to represent the view controller for the table view. To achieve this, select the File -> New -> File… menu option and create a new iOS Cocoa Touch Class file named ResultsTableViewController subclassed from UITableViewController with the Also create XIB file option disabled.

Since the table view will also need a class to represent the table cells, add another new class to the project named ResultsTableCell, subclassing from the UITableViewCell class.

Repeat the above steps to add a third class named RouteViewController subclassed from UIViewController with the Also create XIB file option disabled.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Configuring the Results Table View

Select the Main.storyboard file and drag and drop a Table View Controller object from the Library panel so that it is positioned to the right of the existing View Controller scene in the storyboard canvas (Figure 70-1):

Figure 70-1

With the new controller selected, display the Identity Inspector and change the class from UITableViewController to ResultsTableViewController.

Select the prototype cell at the top of the table view and change the class setting from UITableViewCell to ResultsTableCell. Then, switch to the Attributes Inspector with the table cell still selected and set the Identifier property to resultCell.

Drag two Label objects onto the prototype cell and position them as outlined in Figure 70-2, stretching them to extend to fill the cell’s width.

Figure 70-2

Shift-Click on the two Label views so that both are selected, display the Auto Layout Resolve Auto Layout Issues menu, and select the Reset to Suggested Constraints option listed under Selected Views.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Display the Assistant Editor, make sure that it displays the ResultsTableCell.swift file, and then establish outlets from the two labels named nameLabel and phoneLabel, respectively.

Next, edit the ResultsTableViewController.swift file and modify it to import the MapKit Framework and declare an array into which will be placed the MKMapItem objects representing the local search results:

import UIKit
import MapKit

class ResultsTableViewController: UITableViewController {

    var mapItems: [MKMapItem]?
.
.
}Code language: Swift (swift)

Next, edit the file to modify the data source and delegate methods so that the table is populated with the location information when displayed (removing the #warning lines during the editing process). Note that the comment markers (/* and */) will need to be removed from around the tableView(_:cellForRowAt:) method:

override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return mapItems?.count ?? 0
}


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(
        withIdentifier: "resultCell", for: indexPath) as! ResultsTableCell
    
    // Configure the cell...
    let row = indexPath.row
    
    if let item = mapItems?[row] {
        cell.nameLabel.text = item.name
        cell.phoneLabel.text = item.phoneNumber
    }
    return cell
}Code language: Swift (swift)

With the results table view configured, the next step is to add a segue from the first scene to this scene.

Implementing the Result Table View Segue

Select the Main.storyboard file and drag an additional Bar Button Item from the Library panel to the toolbar in the Map Sample View Controller scene. Double-click on this new button and change the text to Details:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 70-3

Click on the Details button to select it (it may be necessary to click twice since the first click will select the Toolbar). Then, establish a segue by Clicking on the Details button, dragging to the Results Table View Controller, and selecting Show from the Action Segue menu.

When the segue is triggered, the mapItems property of the ResultsTableViewController instance needs to be updated with the array of locations created by the local search. This can be performed in the prepare(for segue:) method, which needs to be implemented in the ViewController.swift file as follows:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    let destination = segue.destination as!
                    ResultsTableViewController

    destination.mapItems = self.matchingItems
}Code language: Swift (swift)

With the Results scene complete, compile and run the app on a device or simulator. Perform a search for a business type that returns valid results before selecting the Details toolbar button. The results table should subsequently appear (Figure 70-4), listing the names and phone numbers for the matching locations:

Figure 70-4

Adding the Route Scene

The last task is to display a second map view and draw the route from the user’s current location to the location selected from the results table. The class for this scene (RouteViewController) was added earlier in the chapter, so the next step is to add a scene to the storyboard and associate it with this class.

Begin by selecting the Main.storyboard file and dragging a View Controller item from the Library panel to position it to the right of the Results Table View Controller scene (Figure 70-5). With the new view controller scene selected (so that it appears with a blue border), display the Identity Inspector and change the class from UIViewController to RouteViewController.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 70-5

Drag and drop a MapKit View object into the new view controller scene and position it to occupy the entire view. Then, using the Auto Layout Add New Constraints menu, set Spacing to nearest neighbor constraints of 0 on all four sides of the view with the Constrain to margins option switched off.

Display the Assistant Editor, make sure it displays the content of the RouteViewController.swift file, and then establish an outlet from the map view instance named routeMap. Then, remaining in the RouteViewController. swift file, add an import directive to the MapKit framework, a property into which will be stored a reference to the destination map item, and a declaration that this class implements the MKMapViewDelegate protocol. With these changes implemented, the file should read as follows:

import UIKit
import MapKit

class RouteViewController: UIViewController {

    var destination: MKMapItem?
.
.
}Code language: Swift (swift)

Now that the route scene has been added, it is time to add some code to it to establish the current location and generate and draw the route on the map.

Identifying the User’s Current Location

Remaining within the RouteViewController.swift file, modify the viewDidLoad method to display the user’s current location in the map view and set this class as the delegate for the map view:

override func viewDidLoad() {
     super.viewDidLoad()
     routeMap.delegate = self
     routeMap.showsUserLocation = true
}Code language: Swift (swift)

The app will need to change the displayed map view region to be centered on the user’s current location. One way to obtain this information would be to access the userLocation property of the MapView instance. The problem with this approach is that it is impossible to know when the map object calculates the current location. This exposes the app to the risk that an attempt to set the region will be made before the location information has been identified. To avoid this problem, the requestLocation method of a CLLocationManager instance will instead be used. Since this method triggers a delegate call when the current location has been obtained, we can safely put the code to use the location within that delegate method.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Begin by importing the CoreLocation framework into the RouteViewController.swift file and declaring the class as implementing both the MKMapViewDelegate and CLLocationManagerDelegate protocols. A constant referencing a CLLocationManager object and a variable in which to store the current location also needs to be declared:

import UIKit
import MapKit
import CoreLocation

class RouteViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {

    var destination: MKMapItem?
    @IBOutlet weak var routeMap: MKMapView!
    var locationManager: CLLocationManager = CLLocationManager()
    var userLocation: CLLocation?
.
.
.Code language: Swift (swift)

Next, implement the two Core Location delegate methods:

func locationManager(_ manager: CLLocationManager, 
       didUpdateLocations locations: [CLLocation]) {
    userLocation = locations[0]
    self.getDirections()

}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    print(error.localizedDescription)
}Code language: Swift (swift)

Next, add code to the viewDidLoad method to identify the current location, thereby triggering a call to the didUpdateLocations delegate method:

override func viewDidLoad() {
    super.viewDidLoad()
    routeMap.delegate = self
    routeMap.showsUserLocation = true
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.delegate = self
    locationManager.requestLocation()
}Code language: Swift (swift)

Getting the Route and Directions

Clearly, the last task performed by the Core Location didUpdateLocations method is to call another method named getDirections which now also needs to be implemented:

func getDirections() {
    
    let request = MKDirections.Request()
    request.source = MKMapItem.forCurrentLocation()
    
    if let destination = destination {
        request.destination = destination
    }
    
    request.requestsAlternateRoutes = false
    
    let directions = MKDirections(request: request)
    
    directions.calculate(completionHandler: {(response, error) in
        
        if let error = error {
            print(error.localizedDescription)
        } else {
            if let response = response {
                self.showRoute(response)
            }
        }
    })
}Code language: Swift (swift)

This code largely matches that outlined at the start of the chapter, as is the case with the implementation of the showRoute method, which also now needs to be implemented in the RouteViewController.swift file along with the corresponding mapView rendererFor overlay method:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func showRoute(_ response: MKDirections.Response) {
    
    for route in response.routes {
        
        routeMap.addOverlay(route.polyline,
                         level: MKOverlayLevel.aboveRoads)
        
        for step in route.steps {
            print(step.instructions)
        }
    }
    
    if let coordinate = userLocation?.coordinate {
        let region =
            MKCoordinateRegion(center: coordinate,
                 latitudinalMeters: 2000, longitudinalMeters: 2000)
        routeMap.setRegion(region, animated: true)
    }
}

func mapView(_ mapView: MKMapView, rendererFor
        overlay: MKOverlay) -> MKOverlayRenderer {
    let renderer = MKPolylineRenderer(overlay: overlay)

    renderer.strokeColor = UIColor.blue
    renderer.lineWidth = 5.0
    return renderer
}Code language: Swift (swift)

The showRoute method adds the polygon for the route as an overlay to the map view, outputs the turn-by-turn steps to the console, and zooms in to the user’s current location.

Establishing the Route Segue

All that remains to complete the app is to establish the segue between the results table cell and the route view. This will also require the implementation of the prepare(for segue:) method to pass the map item for the destination to the route scene.

Select the Main.storyboard file followed by the table cell in the Result Table View Controller scene (ensure the actual cell and not the view or one of the labels is selected). Right-click on the prototype cell and drag the line to the Route View Controller scene. Release the line and select Show from the resulting menu.

Finally, edit the ResultsTableViewController.swift file and implement the prepare(for segue:) method so that the destination property matches the location associated with the selected table row:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let routeViewController = segue.destination
        as! RouteViewController
    
    if let indexPath = self.tableView.indexPathForSelectedRow,
        let destination = mapItems?[indexPath.row] {
            routeViewController.destination = destination
    }
}Code language: Swift (swift)

Testing the App

Build and run the app on a suitable iOS device and perform a local search. Once search results have been returned, select the Details button to display the list of locations. Selecting a location from the list should now cause a second map view to appear containing the user’s current location and the route from there to the selected location drawn in blue, as demonstrated in Figure 70-6:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 70-6

A review of the Xcode console should also reveal that the turn-by-turn directions have been output, for example:

Proceed to Infinite Loop
Turn right
Turn right onto Infinite Loop
Turn right onto Infinite Loop
Turn right onto N De Anza Blvd
Turn right to merge onto I-280 S
Take exit 10 onto Wolfe Road
Turn right onto N Wolfe Rd
Turn left onto Bollinger Rd
Turn right
The destination is on your leftCode language: plaintext (plaintext)

Summary

The MKDirections class was added to the MapKit Framework for iOS 7, allowing directions from one location to another to be requested from Apple’s mapping servers. Information returned from a request includes the text for turn-by-turn directions, the coordinates at which each journey step will take place, and the polygon data needed to draw the route as a map view overlay.


Categories