© Jeffrey Linwood 2020
J. LinwoodBuild Location Apps on iOS with Swifthttps://doi.org/10.1007/978-1-4842-6083-8_14

14. Turn-by-Turn Navigation with Mapbox

Jeffrey Linwood1 
(1)
Austin, TX, USA
 

In addition to the Mapbox mapping SDK for iOS, Mapbox offers a turn-by-turn navigation user interface SDK for iOS as well. Typically, this style of navigation would be used for an app that provides built-in driving directions, such as a rideshare service or a package delivery service.

We will build a simple navigation app in this chapter that routes from the user’s current location to a destination. We will use the standard turn-by-turn user interface that Mapbox provides, known as the Mapbox Navigation framework. If you would like to build your own user interface, you can use the Mapbox Core Navigation framework to power that user interface with route updates. The Mapbox Navigation framework is built on top of the Mapbox Core Navigation framework.

In either case, your application uses the Mapbox Directions API to create routes between origins and destinations. As with other Mapbox APIs, check the pricing for the Directions API before building it into your application, to make sure that you understand how the free tier and tiered pricing works.

Setting up your app project

Before we can dive into the Mapbox Navigation framework, we will need to create a new iOS application project, set it up with CocoaPods, and then add some capabilities and Info.plist entries.

Start by creating a new iOS Single View Application in Xcode. Name the app MapboxNavigationApp. Choose the same defaults as we have used in previous projects, to use storyboard/UIKit and Swift.

Setting up CocoaPods

Once you generate the iOS application, close the project in Xcode, and open the command line in the same directory as the project. We will initialize CocoaPods for this project and then edit the Podfile to include the Mapbox Navigation framework. Generate a Podfile with the following command:
pod init
Once you create a Podfile, open it up in a text editor, and include the Mapbox Navigation framework as a dependency. Your complete Podfile should look like Listing 14-1.
target 'MapboxNavigationApp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!
  # Pods for MapboxNavigationApp
  pod 'MapboxNavigation', '~> 0.39.0'
end
Listing 14-1

Podfile for app that uses the Mapbox Navigation framework

Close the text editor and save the Podfile. Then from the command line, install the dependencies with the following command:
pod install

After you install the CocoaPods, you need to open the MapboxNavigationApp.xcworkspace workspace in Xcode, not the Xcode project.

Adding entries to Info.plist

The next step that we need to do to use the Mapbox SDK is to add two entries to the Info.plist file in your Xcode workspace. Open up that file, and add these two properties:
  • MGLMapboxAccessToken

  • Privacy - Location When In Use Usage Description

The access token entry can be the same access token you have already used in previous Mapbox chapters (see Chapter 11 for instructions). The location usage description will appear to the user the first time the app accesses their location. A sample description might be “This app will use your location to provide in-app navigation and to improve the map”.

This takes care of the Info.plist file settings. We also need to add capabilities to the application so that the app works in background mode.

Adding required capabilities

The Mapbox Navigation framework requires two different background mode capabilities. If you don’t set the background audio mode, the app will crash immediately and tell you to add it. While you can add these background modes through the Info.plist file, you can also add them as capabilities through the Xcode project.

Select the MapboxNavigationApp target in Xcode, and then select the Signing & Capabilities tab. Click the + Capability button to add a capability. Choose Background Modes from the list of capabilities, as shown in Figure 14-1.
../images/485790_1_En_14_Chapter/485790_1_En_14_Fig1_HTML.jpg
Figure 14-1

Adding background mode capability to app target

Select Audio, Airplay, and Picture in Picture, as well as Location updates (Figure 14-2).
../images/485790_1_En_14_Chapter/485790_1_En_14_Fig2_HTML.jpg
Figure 14-2

Selecting background modes for navigation

Now we have everything set up to use the Mapbox Navigation framework. Let’s move on to the Mapbox Directions API.

Using the Mapbox Directions API

Your app needs to calculate a route between two or more different waypoints, using the Mapbox Directions API. While you can make HTTPS calls to the Directions API directly, it’s much easier to use the helper classes in the MapboxDirections framework.

The key classes that you need to get started with the Mapbox Directions API are Waypoint, RouteOptions, Directions, and Route.

Waypoints are the places on the route. You can have up to 25 waypoints for directions when cycling or walking. You may also have up to 25 waypoints if you are driving, but don’t want the route to consider traffic. If you do want to consider traffic conditions, you can only have two or three waypoints.

Route options consist of the waypoints along with the type of transportation used – walking, cycling, driving, or driving considering traffic. This also includes any settings to consider when planning the route, such as avoiding toll roads or ferries or avoiding U-turns. You can also ask for the individual steps within each leg of the route.

When we use the turn-by-turn navigation user interface in the MapboxNavigation framework, we will use a subclass of RouteOptions named NavigationRouteOptions.

The Directions class represents the Directions API. Using a shared single instance of the class, you can call the API asynchronously using the calculate(_:completionHandler:) method. Pass the route options as the first argument and a closure as the second argument.

Last, a successful directions call will pass at least one route as argument to the completion handler. A route will have one leg (a RouteLeg object) if it has two waypoints and one leg for every additional waypoint.

Putting all of this together, we can write two simple methods that will create the waypoints for our directions request and then make the call to the directions API.

The first method (Listing 14-2) creates the waypoints, in this case a trip between Austin, Texas, and the Alamo in San Antonio, Texas. You could use the user’s current location as the first waypoint if you wanted.
func createWaypoints() -> [Waypoint] {
  let austinCoordinate = CLLocationCoordinate2D(
              latitude: 30.27, longitude: -97.74)
  let alamoCoordinate = CLLocationCoordinate2D(
      latitude: 29.426, longitude: -98.486)
  let austin = Waypoint(coordinate: austinCoordinate,
                        name: "Austin")
  let alamo = Waypoint(coordinate: alamoCoordinate,
                      name: "The Alamo")
  return [austin, alamo]
}
Listing 14-2

Create waypoints for Mapbox directions

Add that createWaypoints() method to your ViewController class. You can also see that constructing a waypoint is fairly straightforward.

The getDirections() method in Listing 14-3 passes those waypoints into a new RouteOptions instance and then calls the Mapbox Directions API. This will fail if you have not set the Mapbox access token properly in your project. We then iterate through each step in the only leg of the route. If we had more than two waypoints, we would have additional legs. There are many different properties on the route, leg, and step objects such as estimated time, distance in meters, and headings (for steps) that you can inspect on your own – some may be useful for a custom user interface, and others may be unnecessary.
func getDirections() {
  let waypoints = createWaypoints()
  let options = RouteOptions(waypoints: waypoints,
                  profileIdentifier: .automobile)
  options.includesSteps = true
  Directions.shared.calculate(options) {
    (waypoints, routes, error) in
    guard let route = routes?.first else {
        print(error ?? "No Error")
        return
    }
    guard let firstLeg = route.legs.first else {
        return
    }
    print(firstLeg.name)
    for step in firstLeg.steps {
        print(step.instructions)
    }
  }
}
Listing 14-3

Getting directions from the Mapbox Directions API

Try this function out by calling getDirections() from your viewDidLoad() method and then looking for output in the console.

You could build on top of the preceding function yourself to add the coordinates from the route onto your Mapbox map as a polyline. Instead, we are going to use the much simpler approach of using the prebuilt turn-by-turn navigation user interface in the MapboxNavigation framework.

Displaying the navigation user interface

To display the default navigation user interface, we need to create a NavigationViewController and then present it modally from our ViewController class. We can either do this completely programmatically or add a storyboard reference for the navigation view controller and then configure it in the prepareForSegue method of our view controller. We won’t use the storyboard on this project. Also, just for clarity, this is a different class and serves a different purpose than the UINavigationController class from Apple’s UIKit framework.

We are going to build on the code we discussed earlier, but make a few changes. For instance, we can keep the way that we create waypoints, but we are going to use the NavigationRouteOptions class instead of RouteOptions – the navigation route options comes with configuration optimized for turn-by-turn navigation. Instead of printing out some of the route properties, we are going to create an instance of the NavigationViewController class with the route as an argument. After we create that instance, we present it modally as a full-screen view controller. There is a built-in dismiss button at the bottom of the navigation view controller that would return the user to our original view controller.

We also take the opportunity to add a feature to our route request, which is to avoid toll roads. You can also choose to avoid ferries, motorways, restricted roads, or tunnels. All of these are enumerated on the MBRoadClasses class. Only one of these may be avoided, not a combination, even though the API allows for an array of road classes.

Listing 14-4 is our startNavigation() method . You can see how it mostly differs from the existing getDirections() method in the closure.
func startNavigation() {
  let waypoints = createWaypoints()
  let options = NavigationRouteOptions(
                  waypoints:waypoints)
  options.roadClassesToAvoid = [.toll]
  Directions.shared.calculate(options) {
    (waypoints, routes, error) in
    guard let route = routes?.first else {
        print(error ?? "No error")
        return
    }
    let navVC = NavigationViewController(for: route)
    navVC.modalPresentationStyle = .fullScreen
    self.present(navVC,
                 animated: true,
                 completion: nil)
  }
}
Listing 14-4

Start navigation with turn-by-turn directions in a user interface

Call the startNavigation() method from your viewDidLoad() method. You can replace the getDirections() method we created in Chapter 13. You will see a map view with instructions, an estimated time of arrival, and the remaining distance, similar to Figure 14-3.
../images/485790_1_En_14_Chapter/485790_1_En_14_Fig3_HTML.jpg
Figure 14-3

Turn-by-turn navigation in the app

Now that you have the user interface up and running, explore how it works – you can swipe left and right at the top to navigate between different steps in the route. Press the x button in the lower right to dismiss the navigation view controller when you are finished.

Using the Mapbox simulated navigation

While you can certainly use a GPX file to simulate navigation with the iOS Simulator, you may want to use the simulation that Mapbox provides for a route. This is useful for testing, as it sticks to the route you asked for, and lets you change the speed, as you can see in Figure 14-4.
../images/485790_1_En_14_Chapter/485790_1_En_14_Fig4_HTML.jpg
Figure 14-4

Simulating navigation, sped up to nine times normal speed

To use simulation with Mapbox, you will need to modify the startNavigation() method. Rather than creating the NavigationViewController with just the route, you will need to create a navigation service, use that to populate navigation options, and then pass that to the NavigationViewController.

The navigation service (MapboxNavigationService) gets instantiated with the route and the .always enum for the simulating argument:
let navService = MapboxNavigationService(
      route: route, simulating: .always)
Use that navigation service to create an instance of NavigationOptions:
let navOptions = NavigationOptions(
  navigationService: navService)
We will pass those options to the navigation view controller, along with the route. The route itself does not change when you use navigation simulation:
let navVC = NavigationViewController(for: route,
                                options: navOptions)
Putting the preceding lines of code into our startNavigation() method and replacing the call to create the Mapbox navigation view controller, our final method looks like Listing 14-5.
func startNavigation() {
  let waypoints = createWaypoints()
  let options = NavigationRouteOptions(
      waypoints:waypoints)
  options.roadClassesToAvoid = [.toll]
  Directions.shared.calculate(options) {
    (waypoints, routes, error) in
    guard let route = routes?.first else {
      print(error ?? "No error")
      return
    }
    let navService = MapboxNavigationService(
      route: route, simulating: .always)
    let navOptions = NavigationOptions(
      navigationService: navService)
    let navVC = NavigationViewController(for: route,
                                          options: navOptions)
    navVC.modalPresentationStyle = .fullScreen
    self.present(navVC,
                  animated: true,
                  completion: nil)
  }
}
Listing 14-5

Starting navigation with a simulated drive

After you replace your startNavigation() method with the Swift code in Listing 14-5, run your project. You should expect to see a sped-up drive through the route between your start and finish.

Customizing the navigation experience

If you don’t like the display of the app, you can create custom styles for the user interface, including day and night variants. These styles can use your Mapbox map styles for tiles, and they can also use the UIAppearance protocol to style individual user interface elements. For instance, you could use this to match the colors you use for your brand, so the navigation looks like it belongs with the app.

There are many other features built into the Navigation SDK as well, such as CarPlay support, spoken voice options, and determining which side of the road to use for a waypoint (useful for rideshare or delivery apps).

If you like, you can create your own navigation user interface using the MapboxCoreNavigation framework. You would need to listen for routeControllerProgressDidChange notifications. These notifications contain location updates in the form of a RouteProgress class that you would use to update your user interface. You can get information about the distance traveled, distance remaining, remaining legs and steps, and other information. You can also get more granular information about progress in the current leg and the current step. The current step progress provides information about the next intersection, the distance to the next intersection, the distance until the next maneuver a user makes, and other information you could use for a user interface.

Conclusion

If the turn-by-turn navigation user experience helps your application, using Mapbox’s prebuilt components makes this very easy. Compared to building your own solution on top of another directions API, you can have a simpler experience for the user and less code to maintain within your application.

In Chapter 15, we look at Mapbox’s offline map capabilities – this is one of the areas where Mapbox provides a technology solution that other mapping SDKs don’t replicate.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.78.30