Using MKDirections to get iOS 8 Map Directions and Routes

From Techotopia
Revision as of 18:07, 1 February 2016 by Neil (Talk | contribs) (Text replacement - "<google>ADSDAQBOX_FLOW</google>" to "<htmlet>adsdaqbox_flow</htmlet>")

Jump to: navigation, search
PreviousTable of ContentsNext
Working with MapKit Local Search in iOS 8 and SwiftAn Introduction to Extensions in iOS 8


<google>BUY_IOS8</google>


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 8 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 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.setSource(MKMapItem.mapItemForCurrentLocation())
request.setDestination(destination!)
request.requestsAlternateRoutes = false

let directions = MKDirections(request: request)

directions.calculateDirectionsWithCompletionHandler({(response: 
		MKDirectionsResponse!, error: NSError!) in

            if error != nil {
                // Handle error
            } 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 as! [MKRoute] {

        routeMap.addOverlay(route.polyline, 
				level: MKOverlayLevel.AboveRoads)
    }
}

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 rendererForOverlay 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!, rendererForOverlay 
		overlay: MKOverlay!) -> MKOverlayRenderer! {
    let renderer = MKPolylineRenderer(overlay: overlay)

    renderer.strokeColor = UIColor.blueColor()
    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:

<rpe> routeMap.delegate = self </pre>

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

for step in route.steps {
    println(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 8, 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 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 MapSampleViewController scene in the storyboard canvas (Figure 78 1):


Xcode map sample tableview added.png

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


Ios 8 map sample cell labels.png

Figure 78-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 cellForRowAtIndexPath method:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    // Return the number of sections.
    return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // Return the number of rows in the section.
    return mapItems.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCellWithIdentifier(
	"resultCell", forIndexPath: 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:

Ios 8 map sample details button.png

Figure 78-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 prepareForSegue method which needs to be implemented in the ViewController.swift file as follows:

override func prepareForSegue(segue: UIStoryboardSegue, 
				sender: AnyObject?) {

    let destination = segue.destinationViewController 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 78-4) listing the names and phone numbers for the matching locations:


iOS 8 MapKit search results

Figure 78-4 <google>BUY_IOS8</google>

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 78 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 8 map sample complete storyboard.png

Figure 78-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 Pin 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, MKMapViewDelegate {

    var destination: MKMapItem?
.
.
}					

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

Getting the Route and Directions

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

override func viewDidLoad() {
     super.viewDidLoad()

     routeMap.showsUserLocation = true
     routeMap.delegate = self
     self.getDirections()
}

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

func getDirections() {

    let request = MKDirectionsRequest()
    request.setSource(MKMapItem.mapItemForCurrentLocation())
    request.setDestination(destination!)
    request.requestsAlternateRoutes = false

    let directions = MKDirections(request: request)

    directions.calculateDirectionsWithCompletionHandler({(response: 
		MKDirectionsResponse!, error: NSError!) in

        if error != nil {
            println("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 rendererForOverlay method:

func showRoute(response: MKDirectionsResponse) {

    for route in response.routes as! [MKRoute] {

        routeMap.addOverlay(route.polyline, 
		level: MKOverlayLevel.AboveRoads)

        for step in route.steps {
            println(step.instructions)
        }
    }
    let userLocation = routeMap.userLocation
    let region = MKCoordinateRegionMakeWithDistance(
		userLocation.location.coordinate, 2000, 2000)

    routeMap.setRegion(region, animated: true)
}

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

    renderer.strokeColor = UIColor.blueColor()
    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 prepareForSegue 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 prepareForSegue method so that the destination property matches the location associated with the selected table row:

override func prepareForSegue(segue: UIStoryboardSegue, 
		sender: AnyObject?) {
    let routeViewController = segue.destinationViewController 
					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 78-6:


A route drawn on an iOS 8 MapView overlay

Figure 78-6


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

2013-08-07 10:19:27.737 MapSample[184:60b] Proceed to Parkview Dr
2013-08-07 10:19:27.739 MapSample[184:60b] At the end of the road, turn left onto Parkgreen Ln
2013-08-07 10:19:27.740 MapSample[184:60b] Turn right onto NC Highway 58
2013-08-07 10:19:27.742 MapSample[184:60b] Turn right onto High Meadow Rd
2013-08-07 10:19:27.743 MapSample[184:60b] Turn left onto Wilson Dr
2013-08-07 10:19:27.745 MapSample[184:60b] Turn right onto Morris Pkwy
2013-08-07 10:19:27.789 MapSample[184:60b] 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.


<google>BUY_IOS8</google>



PreviousTable of ContentsNext
Working with MapKit Local Search in iOS 8 and SwiftAn Introduction to Extensions in iOS 8