Difference between revisions of "Using MKDirections to get iOS 8 Map Directions and Routes"

From Techotopia
Jump to: navigation, search
Line 97: Line 97:
  
  
[[File:]]
+
[[File:ios_11_map_sample_table_view_controller.png]]
 
   
 
   
 
Figure 82-1
 
Figure 82-1
Line 109: Line 109:
  
  
[[File:]]
+
[[File:ios_11_map_sample_table_cell.png]]
 
   
 
   
 
Figure 82-2
 
Figure 82-2
Line 166: Line 166:
 
   
 
   
  
[[File:]]
+
[[File:ios_8_map_sample_details_button.png]]
  
 
Figure 82-3
 
Figure 82-3
Line 188: Line 188:
 
   
 
   
  
[[File:]]
+
[[File:ios_11_map_sample_table_view_running.png]]
  
 
Figure 82-4
 
Figure 82-4
 +
  
 
== Adding the Route Scene ==
 
== Adding the Route Scene ==
Line 199: Line 200:
  
  
[[File:]]
+
[[File:ios_11_map_sample_route_controller.png]]
 
   
 
   
 
Figure 82-5
 
Figure 82-5
Line 373: Line 374:
  
  
[[File:]]
+
[[File:ios_11_map_sample_route_drawn.png]]
 
   
 
   
 
Figure 82-6
 
Figure 82-6

Revision as of 20:30, 1 May 2018


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 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.


Contents


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:

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
}

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

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, 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 82-1):


Ios 11 map sample table view controller.png

Figure 82-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 82 2, making sure to stretch them so that they extend to fill the width of the cell.


Ios 11 map sample table cell.png

Figure 82-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.

Next, edit the ResultsTableViewController.swift file and modify it to import the MapKit Framework and to 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]?
.
.
}

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
} 

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:


Ios 8 map sample details button.png

Figure 82-3


Click on the Details button to select it (it may be necessary to click twice since the first click will select the Toolbar). Establish a segue by Ctrl-clicking on the Details button and dragging to the Results Table View Controller and select 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
}

With the Results scene complete, compile and run the application 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 82-4) listing the names and phone numbers for the matching locations:


Ios 11 map sample table view running.png

Figure 82-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 82-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.


Ios 11 map sample route controller.png

Figure 82-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 time to add some code to it to establish the current location and to generate and then 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
}

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()
}

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()
    
    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)
            }
        }
    })
} 

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)
        }
    }
    
    if let coordinate = userLocation?.coordinate {
        let region =
            MKCoordinateRegionMakeWithDistance(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
}

The showRoute method simply 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 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
    
    if let indexPath = self.tableView.indexPathForSelectedRow,
        let destination = mapItems?[indexPath.row] {
            routeViewController.destination = destination
    }
}

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 82-6:


Ios 11 map sample route drawn.png

Figure 82-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 left

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.