Listen for navigation events

Use this guide to enable your app to listen and respond to a variety of events that change as a user navigates along a route. This guide does not cover defining a route, only responding to events along a route.

Overview

The Navigation SDK for iOS provides you with listeners associated with the location of the user and conditions along the route and important time and distance data. On the view controller of the map, your app needs to adopt the protocols for these listeners: GMSRoadSnappedLocationProviderListener and GMSNavigatorListener.

This list shows the listener methods available for navigation events:

See the code

/*
 * Copyright 2020 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import GoogleNavigation
import UIKit

class ViewController: UIViewController,
  GMSNavigatorListener,
  GMSRoadSnappedLocationProviderListener
{

  var mapView: GMSMapView!
  var locationManager: CLLocationManager!

  override func loadView() {

    locationManager = CLLocationManager()

    let camera = GMSCameraPosition.camera(withLatitude: 47.67, longitude: -122.20, zoom: 14)
    mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)

    // Add listeners for GMSNavigator and GMSRoadSnappedLocationProvider.
    mapView.navigator?.add(self)
    mapView.roadSnappedLocationProvider?.add(self)

    // Set the time update threshold (seconds) and distance update threshold (meters).
    mapView.navigator?.timeUpdateThreshold = 10
    mapView.navigator?.distanceUpdateThreshold = 100

    // Show the terms and conditions.
    let companyName = "Ride Sharing Co."
    GMSNavigationServices.showTermsAndConditionsDialogIfNeeded(
      withCompanyName: companyName
    ) { termsAccepted in
      if termsAccepted {
        // Enable navigation if the user accepts the terms.
        self.mapView.isNavigationEnabled = true

        // Request authorization to use location services.
        self.locationManager.requestAlwaysAuthorization()

        // Request authorization for alert notifications which deliver guidance instructions
        // in the background.
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) {
          granted, error in
          // Handle denied authorization to display notifications.
          if !granted || error != nil {
            print("Authorization to deliver notifications was rejected.")
          }
        }
      } else {
        // Handle the case when the user rejects the terms and conditions.
      }
    }

    view = mapView

    makeButton()
  }

  // Create a route and start guidance.
  @objc func startNav() {
    var destinations = [GMSNavigationWaypoint]()
    destinations.append(
      GMSNavigationWaypoint.init(
        placeID: "ChIJnUYTpNASkFQR_gSty5kyoUk",
        title: "PCC Natural Market")!)
    destinations.append(
      GMSNavigationWaypoint.init(
        placeID: "ChIJJ326ROcSkFQRBfUzOL2DSbo",
        title: "Marina Park")!)

    mapView.navigator?.setDestinations(destinations) { routeStatus in
      guard routeStatus == .OK else {
        print("Handle route statuses that are not OK.")
        return
      }
      self.mapView.navigator?.isGuidanceActive = true
      self.mapView.cameraMode = .following
      self.mapView.locationSimulator?.simulateLocationsAlongExistingRoute()
    }

    mapView.roadSnappedLocationProvider?.startUpdatingLocation()
  }

  // Listener to handle continuous location updates.
  func locationProvider(
    _ locationProvider: GMSRoadSnappedLocationProvider,
    didUpdate location: CLLocation
  ) {
    print("Location: \(location.description)")
  }

  // Listener to handle speeding events.
  func navigator(
    _ navigator: GMSNavigator, didUpdateSpeedingPercentage percentageAboveLimit: CGFloat
  ) {
    print("Speed is \(percentageAboveLimit) above the limit.")
  }

  // Listener to handle arrival events.
  func navigator(_ navigator: GMSNavigator, didArriveAt waypoint: GMSNavigationWaypoint) {
    print("You have arrived at: \(waypoint.title)")
    mapView.navigator?.continueToNextDestination()
    mapView.navigator?.isGuidanceActive = true
  }

  // Listener for route change events.
  func navigatorDidChangeRoute(_ navigator: GMSNavigator) {
    print("The route has changed.")
  }

  // Listener for time to next destination.
  func navigator(_ navigator: GMSNavigator, didUpdateRemainingTime time: TimeInterval) {
    print("Time to next destination: \(time)")
  }

  // Delegate for distance to next destination.
  func navigator(
    _ navigator: GMSNavigator,
    didUpdateRemainingDistance distance: CLLocationDistance
  ) {
    let miles = distance * 0.00062137
    print("Distance to next destination: \(miles) miles.")
  }

  // Delegate for traffic updates to next destination
  func navigator(
    _ navigator: GMSNavigator,
    didUpdate delayCategory: GMSNavigationDelayCategory
  ) {
    print("Delay category to next destination: \(String(describing: delayCategory)).")
  }

  // Delegate for suggested lighting mode changes.
  func navigator(
    _ navigator: GMSNavigator,
    didChangeSuggestedLightingMode lightingMode: GMSNavigationLightingMode
  ) {
    print("Suggested lighting mode has changed: \(String(describing: lightingMode))")

    // Change to the suggested lighting mode.
    mapView.lightingMode = lightingMode
  }

  // Add a button to the view.
  func makeButton() {
    // Start navigation.
    let navButton = UIButton(frame: CGRect(x: 5, y: 150, width: 200, height: 35))
    navButton.backgroundColor = .blue
    navButton.alpha = 0.5
    navButton.setTitle("Start navigation", for: .normal)
    navButton.addTarget(self, action: #selector(startNav), for: .touchUpInside)
    self.mapView.addSubview(navButton)
  }

}
/*
 * Copyright 2020 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#import "ViewController.h"
@import GoogleNavigation;

@interface ViewController () <GMSNavigatorListener, GMSRoadSnappedLocationProviderListener>

@end

@implementation ViewController {
  GMSMapView *_mapView;
  CLLocationManager *_locationManager;
}

- (void)loadView {

  _locationManager = [[CLLocationManager alloc] init];

  GMSCameraPosition *camera = [GMSCameraPosition cameraWithLatitude:47.67
                                                          longitude:-122.20
                                                               zoom:14];
  _mapView = [GMSMapView mapWithFrame:CGRectZero camera:camera];

  // Add listeners for GMSNavigator and GMSRoadSnappedLocationProvider.
  [_mapView.navigator addListener:self];
  [_mapView.roadSnappedLocationProvider addListener:self];

  // Set the time update threshold (seconds) and distance update threshold (meters).
  _mapView.navigator.timeUpdateThreshold = 10;
  _mapView.navigator.distanceUpdateThreshold = 100;

  // Show the terms and conditions.
  NSString *companyName = @"Ride Sharing Co.";
  [GMSNavigationServices
   showTermsAndConditionsDialogIfNeededWithCompanyName:companyName
                                              callback:^(BOOL termsAccepted) {
     if (termsAccepted) {
       // Enable navigation if the user accepts the terms.
       _mapView.navigationEnabled = YES;

       // Request authorization to use location services.
       [_locationManager requestAlwaysAuthorization];

     } else {
       // Handle the case when the user rejects the terms and conditions.
     }
   }];

  self.view = _mapView;

  [self makeButton];
}

// Create a route and initiate navigation.
- (void)startNav {
  NSArray<GMSNavigationWaypoint *> *destinations =
  @[[[GMSNavigationWaypoint alloc] initWithPlaceID:@"ChIJnUYTpNASkFQR_gSty5kyoUk"
                                             title:@"PCC Natural Market"],
    [[GMSNavigationWaypoint alloc] initWithPlaceID:@"ChIJJ326ROcSkFQRBfUzOL2DSbo"
                                             title:@"Marina Park"]];

  [_mapView.navigator setDestinations:destinations
                             callback:^(GMSRouteStatus routeStatus){
                               _mapView.navigator.guidanceActive = YES;
                               _mapView.navigator.sendsBackgroundNotifications = YES;
                               _mapView.cameraMode = GMSNavigationCameraModeFollowing;
                               [_mapView.locationSimulator simulateLocationsAlongExistingRoute];
                             }];

  [_mapView.roadSnappedLocationProvider startUpdatingLocation];
}

#pragma mark - GMSNavigatorListener
// Listener for continuous location updates.
- (void)locationProvider:(GMSRoadSnappedLocationProvider *)locationProvider
      didUpdateLocation:(CLLocation *)location {
  NSLog(@"Location: %@", location.description);
}

// Listener to handle speeding events.
- (void)navigator:(GMSNavigator *)navigator
    didUpdateSpeedingPercentage:(CGFloat)percentageAboveLimit {
  NSLog(@"Speed is %f percent above the limit.", percentageAboveLimit);
}

// Listener to handle arrival events.
- (void)navigator:(GMSNavigator *)navigator didArriveAtWaypoint:(GMSNavigationWaypoint *)waypoint {
  NSLog(@"You have arrived at: %@", waypoint.title);
  [_mapView.navigator continueToNextDestination];
  _mapView.navigator.guidanceActive = YES;
}

// Listener for route change events.
- (void)navigatorDidChangeRoute:(GMSNavigator *)navigator {
  NSLog(@"The route has changed.");
}

// Listener for time to next destination.
- (void)navigator:(GMSNavigator *)navigator didUpdateRemainingTime:(NSTimeInterval)time {
  NSLog(@"Time to next destination: %f", time);
}

// Listener for distance to next destination.
- (void)navigator:(GMSNavigator *)navigator
      didUpdateRemainingDistance:(CLLocationDistance)distance {
  double miles = distance * 0.00062137;
  NSLog(@"%@", [NSString stringWithFormat:@"Distance to next destination: %.2f.", miles]);
}

// Listener for traffic updates for next destination
- (void)navigator:(GMSNavigator *)navigator
    didUpdateDelayCategory:(GMSNavigationDelayCategory)delayCategory {
  NSLog(@"Delay category to next destination: %ld.", delayCategory);
}

// Listener for suggested lighting mode changes.
-(void)navigator:(GMSNavigator *)navigator
      didChangeSuggestedLightingMode:(GMSNavigationLightingMode)lightingMode {
  NSLog(@"Suggested lighting mode has changed: %ld", (long)lightingMode);

  // Change to the suggested lighting mode.
  _mapView.lightingMode = lightingMode;
}

#pragma mark - Programmatic UI elements

// Add a button to the view.
- (void)makeButton {
  // Start navigation.
  UIButton *navButton = [UIButton buttonWithType:UIButtonTypeCustom];
  [navButton addTarget:self
                action:@selector(startNav)
      forControlEvents:UIControlEventTouchUpInside];
  [navButton setTitle:@"Navigate" forState:UIControlStateNormal];
  [navButton setBackgroundColor:[UIColor blueColor]];
  [navButton setAlpha:0.5];
  navButton.frame = CGRectMake(5.0, 150.0, 100.0, 35.0);
  [_mapView addSubview:navButton];
}

@end

Declaring conformance to the required protocols

Before implementing the navigation methods, the view controller must adopt the protocols:

class ViewController: UIViewController, GMSNavigatorListener,
GMSRoadSnappedLocationProviderListener {

@interface ViewController () <GMSNavigatorListener,
GMSRoadSnappedLocationProviderListener>

@end

After adopting the navigation protocols, set the listeners to the view controller. For example, you can add the following code to the viewDidLoad() method.

mapView.navigator?.add(self) mapView.roadSnappedLocationProvider?.add(self)

[_mapView.navigator addListener:self]; [_mapView.roadSnappedLocationProvider
addListener:self];

Receiving or stopping location updates

Location updates are required for showing the user's progress on the map.

The location instance exposes the following properties:

Location property Description
altitude Current altitude.
coordinate.latitude Current road-snapped latitude coordinate.
coordinate.longitude Current road-snapped longitude coordinate.
course Current bearing in degrees.
speed Current speed.
timestamp Date/time of current reading.

To receive continuous location updates, call mapView.roadSnappedLocationProvider.startUpdatingLocation , and use the GMSRoadSnappedLocationProviderListener to handle the didUpdateLocation event.

The following example shows calling startUpdatingLocation:

mapView.roadSnappedLocationProvider.startUpdatingLocation()

[_mapView.roadSnappedLocationProvider startUpdatingLocation];

The following code creates a GMSRoadSnappedLocationProviderListener that handles the didUpdateLocation event.

func locationProvider(_ locationProvider: GMSRoadSnappedLocationProvider,
didUpdate location: CLLocation) { print("Location: \(location.description)") }

-   (void)locationProvider:(GMSRoadSnappedLocationProvider *)locationProvider
    didUpdateLocation:(CLLocation *)location { NSLog(@"Location: %@",
    location.description); }

To receive location updates when the app is in the background, set allowsBackgroundLocationUpdates to true:

mapView.roadSnappedLocationProvider.allowsBackgroundLocationUpdates = true

 _mapView.roadSnappedLocationProvider.allowsBackgroundLocationUpdates = YES;

Detecting arrival events

Your app uses the didArriveAtWaypoint event to detect when a destination has been reached. You can resume guidance and advance to the next waypoint by calling continueToNextDestination(), and then re-enabling guidance. Your app must re-enable guidance after calling continueToNextDestination().

After the app calls continueToNextDestination, the navigator no longer has data about the previous destination. If you want to analyze information about a route leg, you must retrieve this from the navigator prior to calling continueToNextDestination().

The following code example shows a method to handle the didArriveAtWaypoint event:

func navigator(_ navigator: GMSNavigator, didArriveAt waypoint:
GMSNavigationWaypoint) { print("You have arrived at: \(waypoint.title)")
mapView.navigator?.continueToNextDestination()
mapView.navigator?.isGuidanceActive = true }

-   (void)navigator:(GMSNavigator *)navigator
    didArriveAtWaypoint:(GMSNavigationWaypoint *)waypoint { NSLog(@"You have
    arrived at: %@", waypoint.title); [_mapView.navigator
    continueToNextDestination]; _mapView.navigator.guidanceActive = YES; }

Receiving route change updates

To receive a notification whenever the route is changed, create a method to handle the navigatorDidChangeRoute event. You can access the new route by using the routeLegs and currentRouteLeg properties of GMSNavigator.

func navigatorDidChangeRoute(_ navigator: GMSNavigator) { print("The route has
changed.") }

-   (void)navigatorDidChangeRoute:(GMSNavigator *)navigator { NSLog(@"The route
    has changed."); }

Receiving time to destination updates

To receive continuous time to destination updates, create a method to handle the didUpdateRemainingTime event. The time parameter provides the estimated time, in seconds, until the next destination is reached.

func navigator(_ navigator: GMSNavigator, didUpdateRemainingTime time:
TimeInterval) { print("Time to next destination: \(time)") }

-   (void)navigator:(GMSNavigator *)navigator
    didUpdateRemainingTime:(NSTimeInterval)time { NSLog(@"Time to next
    destination: %f", time); }

To set the minimum change in estimated time to the next destination, set the timeUpdateThreshold property on GMSNavigator. The value is specified in seconds. If this property is not set, the services use a default value of one second.

navigator?.timeUpdateThreshold = 10

navigator.timeUpdateThreshold = 10;

Receiving distance to destination updates

To receive continuous distance to destination updates, create a method to handle the didUpdateRemainingDistance event. The distance parameter provides the estimated distance, in meters, to the next destination.

func navigator(_ navigator: GMSNavigator, didUpdateRemainingDistance distance:
CLLocationDistance) { let miles = distance * 0.00062137 print("Distance to next
destination: \(miles) miles.") }

-   (void)navigator:(GMSNavigator *)navigator
    didUpdateRemainingDistance:(CLLocationDistance)distance { double miles =
    distance * 0.00062137; NSLog(@"%@", [NSString stringWithFormat:@"Distance to
    next destination: %.2f.", miles]); }

To set the minimum change in estimated distance to the next destination, set the distanceUpdateThreshold property on GMSNavigator (value is specified in meters). If this property is not set, the services use a default value of one meter.

navigator?.distanceUpdateThreshold = 100

navigator.distanceUpdateThreshold = 100;

Receiving traffic updates

To receive continuous updates of the traffic flow for the remaining route, create a method to handle the didUpdateDelayCategory event. A call to delayCategoryToNextDestination returns GMSNavigationDelayCategory which provides a value of 0 to 3. Updates to the category are based on the current position of the app user. If traffic data is unavailable, GMSNavigationDelayCategory returns 0. The numbers, 1-3, indicate increasing flow from light to heavy.

func navigator(_ navigator: GMSNavigator, didUpdate delayCategory:
GMSNavigationDelayCategory) { print("Traffic flow to next destination:
\(delayCategory)") }

-   (void)navigator:(GMSNavigator *)navigator
    didUpdateDelayCategory:(GMSNavigationDelayCategory)delayCategory {
    NSLog(@"Traffic flow to next destination: %ld", (long)delayCategory); }

The GMSNavigationDelayCategory property exposes the following delay levels:

Delay category Description
GMSNavigationDelayCategoryNoData 0 - Unavailable, no data for traffic or :
the route.
GMSNavigationDelayCategoryHeavy 1 - Heavy.
GMSNavigationDelayCategoryMedium 2 - Medium.
GMSNavigationDelayCategoryLight 3 - Light.

Receiving speeding updates

To receive updates when a driver is exceeding the speed limit, create a method to handle the didUpdateSpeedingPercentage event.

// Listener to handle speeding events. func navigator( _ navigator:
GMSNavigator, didUpdateSpeedingPercentage percentageAboveLimit: CGFloat ) {
print("Speed is \(percentageAboveLimit) above the limit.") }

// Listener to handle speeding events. - (void)navigator:(GMSNavigator
*)navigator didUpdateSpeedingPercentage:(CGFloat)percentageAboveLimit {
NSLog(@"Speed is %f percent above the limit.", percentageAboveLimit); }

Changing suggested lighting mode

To receive updates for estimated changes in lighting, create a method to handle the didChangeSuggestedLightingMode event.

// Define a listener for suggested changes to lighting mode. func navigator(_
navigator: GMSNavigator, didChangeSuggestedLightingMode lightingMode:
GMSNavigationLightingMode) { print("Suggested lighting mode has changed:
\(String(describing: lightingMode))")

 // Make the suggested change. mapView.lightingMode = lightingMode }

// Define a listener for suggested changes to lighting mode.
-(void)navigator:(GMSNavigator *)navigator didChangeSuggestedLightingMode:
(GMSNavigationLightingMode)lightingMode { NSLog(@"Suggested lighting mode has
changed: %ld", (long)lightingMode);

 // Make the suggested change. _mapView.lightingMode = lightingMode; }