Using MKDirections to get iOS 10 Map Directions and Routes
Previous | Table of Contents | Next |
Working with MapKit Local Search in iOS 10 | An iOS 10 MapKit Flyover Tutorial |
Learn SwiftUI and take your iOS Development to the Next Level |
In this, the final chapter covering the MapKit framework, the use of the MKDirections class to obtain directions and route information from within an iOS 10 application will be explored. Having covered the basics, the MapSample tutorial application will be extended to make use of 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 is used to generate 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 MKDirectionsRequest instance. In addition to storing the start and end points, the MKDirectionsRequest class also provides a number of 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 in the form of an MKDirectionsResponse 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 of which contains 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:
Learn SwiftUI and take your iOS Development to the Next Level |
let request = MKDirectionsRequest() 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!) } })
The resulting response can subsequently be used to draw the routes on a map view using the following code:
func showRoute(_ response: MKDirectionsResponse) { for route in response.routes { routeMap.add(route.polyline, level: MKOverlayLevel.aboveRoads) for step in route.steps { print(step.instructions) } } }
The above code simply 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 }
Learn SwiftUI and take your iOS Development to the Next Level |
routeMap.delegate = self
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) }
The above code simply outputs the text instructions for each step of the route. As previously discussed, additional information may also be extracted from the MKRouteStep objects as required by the application.
Having covered the basics of directions and routes in iOS 10, the MapSample application can be extended to put some of this theory into practice.
Adding Directions and Routes to the MapSample Application
The MapSample application 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 the names and phone numbers of all locations 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 application 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, this time 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.
Configuring the Results Table View
Select the Main.storyboard file and drag and drop a Table View Controller object from the Object Library so that it is positioned to the right of the existing View Controller scene in the storyboard canvas (Figure 81-1):
Figure 81-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. With the table cell still selected, switch to the Attributes Inspector and set the Reuse Identifier property to resultCell.
Drag and drop two Label objects onto the prototype cell and position them as outlined in Figure 81 2, making sure to stretch them so that they extend to fill the width of the cell.
Figure 81-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.
Display the Assistant Editor, make sure that it is displaying the ResultsTableCell.swift file and then establish outlets from the two labels named nameLabel and phoneLabel respectively.
Learn SwiftUI and take your iOS Development to the Next Level |
import UIKit import MapKit class ResultsTableViewController: UITableViewController { var mapItems: [MKMapItem]! . . }
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 } 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 let item = mapItems[row] cell.nameLabel.text = item.name cell.phoneLabel.text = item.phoneNumber return cell }
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 and drop an additional Bar Button Item from the Object Library to the toolbar in the Map Sample View Controller scene. Double-click on this new button and change the text to Details:
Figure 81-3
Learn SwiftUI and take your iOS Development to the Next Level |
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 }
With the Results scene complete, compile and run the application on an iOS device. Perform a search for a business type that returns valid results before selecting the Details toolbar button. The results table should subsequently appear (Figure 81-4) listing the names and phone numbers for the matching locations:
Figure 81-4
Adding the Route Scene
The last task is to display a second map view and draw on it 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 and dropping a View Controller item from the Object Library panel so that it is positioned to the right of the Results Table View Controller scene (Figure 81 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.
Figure 81-5
Drag and drop a MapKit View object into the new view controller scene and position it so that it occupies the entire view. 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 is displaying the content of the RouteViewController.swift file and then establish an outlet from the map view instance named routeMap. 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? . . }
Now that the route scene has been added, it is now time to add some code to it to establish the current location and to generate and then draw the route on the map.
Learn SwiftUI and take your iOS Development to the Next Level |
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 }
The app is going to need to change the displayed map view region so that it is 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 not possible to know when the map object has calculated 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.
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? . . . 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) }
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() }
Learn SwiftUI and take your iOS Development to the Next Level |
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 = MKDirectionsRequest() 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!) } }) }
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:
func showRoute(_ response: MKDirectionsResponse) { for route in response.routes { routeMap.add(route.polyline, level: MKOverlayLevel.aboveRoads) for step in route.steps { print(step.instructions) } } let region = MKCoordinateRegionMakeWithDistance(userLocation!.coordinate, 2000, 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 }
Learn SwiftUI and take your iOS Development to the Next Level |
Establishing the Route Segue
All that remains to complete the application 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 (making sure the actual cell and not the view or one of the labels is selected). Ctrl-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 let indexPath = self.tableView.indexPathForSelectedRow! let row = indexPath.row routeViewController.destination = mapItems![row] }
Testing the Application
Build and run the application 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 81-6:
Figure 81-6
A review of the Xcode console should also reveal that the turn-by-turn directions have been output, for example:
Proceed to Parkview Dr At the end of the road, turn left onto Parkgreen Ln Turn right onto NC Highway 58 Turn right onto High Meadow Rd Turn left onto Wilson Dr Turn right onto Morris Pkwy Arrive at the destination
Summary
The MKDirections class was added to the MapKit Framework for iOS 7 and allows 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 step of the journey is to take place and the polygon data needed to draw the route as a map view overlay.
Learn SwiftUI and take your iOS Development to the Next Level |
Previous | Table of Contents | Next |
Working with MapKit Local Search in iOS 10 | An iOS 10 MapKit Flyover Tutorial |