Build a navigation app

VERSION 0.2.3404
PUBLIC PREVIEW

This tutorial shows how to build a simple navigation application using the TomTom Navigation SDK for iOS. The app uses the built-in UI components. You can also build custom components and integrate them with the SDK.

The application displays a map that shows the current location. After the user selects a destination with a long press, the app plans a route and draws it on the map. Navigation is started automatically using the GPS location or a route simulation.

Project set up

The Navigation SDK for iOS is only available upon request. Contact us to get started.

  1. Install Xcode if you don’t already have it.
  2. Create a new project or open an existing one. The application deployment target has to be set to at least 13.0.
  3. Install Cocoapods on your computer.
    sudo gem install cocoapods
  4. Install cocoapods-art tool on your computer.
    sudo gem install cocoapods-art to install cocoapods-art
  5. Because the repository for Navigation SDK is private, you will need to contact us to get access. Once you have obtained access, go to repositories.tomtom.com and log in with your account. Expand the user menu in the top-right corner, and select "Edit profile" → "Generate an Identity Token". Copy your token and put it, together with your login, in ~/.netrc. If the file doesn’t exist, create one and add the following entry:
    1machine repositories.tomtom.com
    2login <YOUR_LOGIN>
    3password <YOUR_TOKEN>
  6. Add a reference to the cocoapods-art repository:
    pod repo-art add tomtom-sdk-cocoapods "https://repositories.tomtom.com/artifactory/api/pods/cocoapods"
  7. Then create a Podfile in the project folder. The pod init command in the project folder can generate a basic podfile.
  8. At the top of the Podfile add the source of the SDK Cocoapods.
    1plugin 'cocoapods-art', :sources => [
    2 'tomtom-sdk-cocoapods'
    3]
  9. Add the modules that your project requires. This tutorial uses the TomTomSDKMapDisplay, TomTomSDKRoutePlannerOnline, TomTomSDKRoutePlanner, TomTomSDKNavigation, TomTomSDKCommonUI, TomTomSDKDefaultTextToSpeech, TomTomSDKTextToSpeechEngine and TomTomSDKTextToSpeech modules.
    1TOMTOM_SDK_VERSION = '0.2.3404'
    2
    3target 'YourAppTarget' do
    4 use_frameworks!
    5 pod 'TomTomSDKMapDisplay', TOMTOM_SDK_VERSION
    6 pod 'TomTomSDKNavigation', TOMTOM_SDK_VERSION
    7 pod 'TomTomSDKRoutePlanner', TOMTOM_SDK_VERSION
    8 pod 'TomTomSDKRoutePlannerOnline', TOMTOM_SDK_VERSION
    9 pod 'TomTomSDKCommonUI', TOMTOM_SDK_VERSION
    10 pod 'TomTomSDKDefaultTextToSpeech', TOMTOM_SDK_VERSION
    11 pod 'TomTomSDKTextToSpeechEngine', TOMTOM_SDK_VERSION
    12 pod 'TomTomSDKTextToSpeech', TOMTOM_SDK_VERSION
    13end
  10. Install the dependencies by executing the following command in the project folder.
    pod install
  11. To update the SDK version, run the command:
    pod repo-art update tomtom-sdk-cocoapods
  12. Open the project’s xcworkspace.

Make sure you have the appropriate TomTom API key. If you don’t have an API Key, go to How to get a TomTom API Key to learn how to create one. Using an invalid API key will cause issues loading the map or running navigation.

Getting started

Create an empty App project with Xcode 13.4 or higher, using a SwiftUI interface that targets iOS 13 and above. Copy the following snippets into the *App.swift file that Xcode generated for the project.

Many of the components in the tutorial build upon each other. The program will only successfully compile at the end of each section coming after Displaying a map.

  1. Create the basic Swift application structure as follows:
    1// System modules
    2import CoreLocation
    3import Foundation
    4import SwiftUI
    5
    6// GO SDK modules
    7import TomTomSDKCommon
    8import TomTomSDKCommonUI
    9import TomTomSDKDefaultTextToSpeech
    10import TomTomSDKLocationProvider
    11import TomTomSDKMapDisplay
    12import TomTomSDKNavigation
    13import TomTomSDKNavigationEngines
    14import TomTomSDKRoute
    15import TomTomSDKRoutePlanner
    16import TomTomSDKRoutePlannerOnline
    17import TomTomSDKRouteReplannerDefault
    18import TomTomSDKTextToSpeech
    19import TomTomSDKTextToSpeechEngine
    20
    21// MARK: - TomTomServices
    22
    23enum TomTomServices {
    24 static func register() {
    25 TomTomSDKMapDisplay.MapsDisplayService.apiKey = Keys.ttServicesAPIKey
    26 }
    27}
    28
    29// MARK: - AppDelegate
    30
    31class AppDelegate: NSObject, UIApplicationDelegate {
    32 func application(
    33 _ application: UIApplication,
    34 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    35 )
    36 -> Bool {
    37 TomTomServices.register()
    38 return true
    39 }
    40}
    41
    42// MARK: - TomTomSDKNavigationUseCaseApp
    43
    44@main
    45struct TomTomSDKNavigationUseCaseApp: App {
    46 @UIApplicationDelegateAdaptor(AppDelegate.self)
    47 var appDelegate
    48
    49 var body: some Scene {
    50 WindowGroup {
    51 MainView()
    52 }
    53 }
    54}
    55
    56// MARK: - MainView
    57
    58struct MainView: View {
    59 // MARK: Internal
    60
    61 var body: some View {
    62 TomTomMapView(contentInsets: $contentInsets)
    63 }
    64
    65 // MARK: Private
    66
    67 @State
    68 private var contentInsets: EdgeInsets = .init()
    69}
    70
    71// MARK: - TomTomMapView
  2. Add a struct to hold your API key:
    1struct Keys {
    2 static let ttServicesAPIKey = "YOUR_API_KEY"
    3}

Key storage in the sourcecode is for demo purposes only. Remember to securely store your API keys when creating your app.

These code snippets set up the App struct and AppDelegate which registers the TomTom API key with the map display and online routing services. The TomTomMapView is then created in the next section.

Displaying a map

This section explains how to display a map and interact with it. Copy the snippets below into your Xcode project to complete this part of the tutorial.

  1. Create the MapView struct:

    1struct TomTomMapView {
    2 var mapView = TomTomSDKMapDisplay.MapView()
    3 var contentInsets: Binding<EdgeInsets>
    4
    5 func updateEdgeInsets(context: Context) {
    6 context.coordinator.setContentInsets(edgeInsets: contentInsets.wrappedValue)
    7 }
    8}
    9
    10// MARK: UIViewRepresentable
  2. Extend the TomTomMapView to conform to the UIViewRepresentable protocol. This allows you to use the MapView in SwiftUI:

    1extension TomTomMapView: UIViewRepresentable {
    2 typealias UIViewType = TomTomSDKMapDisplay.MapView
    3
    4 func makeUIView(context: Context) -> TomTomSDKMapDisplay.MapView {
    5 mapView.delegate = context.coordinator
    6 mapView.currentLocationButtonVisibilityPolicy = .hiddenWhenCentered
    7 mapView.compassButtonVisibilityPolicy = .visibleWhenNeeded
    8 return mapView
    9 }
    10
    11 func updateUIView(_: TomTomSDKMapDisplay.MapView, context: Context) {
    12 updateEdgeInsets(context: context)
    13 }
    14
    15 func makeCoordinator() -> MapCoordinator {
    16 MapCoordinator(mapView)
    17 }
    18}
    19
    20// MARK: - MapCoordinator
  3. Create the MapCoordinator, which facilitates communication from the UIView to the SwiftUI environments.

    1class MapCoordinator: NSObject {
    2 // MARK: Lifecycle
    3
    4 init(_ mapView: TomTomSDKMapDisplay.MapView) {
    5 self.mapView = mapView
    6 }
    7
    8 // MARK: Internal
    9
    10 func setContentInsets(edgeInsets: EdgeInsets) {
    11 mapView.contentInsets = NSDirectionalEdgeInsets(
    12 top: edgeInsets.top,
    13 leading: edgeInsets.leading,
    14 bottom: edgeInsets.bottom,
    15 trailing: edgeInsets.trailing
    16 )
    17 }
    18
    19 // MARK: Private
    20
    21 private var mapView: TomTomSDKMapDisplay.MapView
    22 private var map: TomTomSDKMapDisplay.TomTomMap! // Set in onMapReady callback
    23 private var cameraUpdated = false
    24 private let routePlanner = TomTomSDKRoutePlannerOnline.OnlineRoutePlanner(apiKey: Keys.ttServicesAPIKey)
    25 private var navigation: TomTomSDKNavigation.Navigation?
    26 private let textToSpeech = TextToSpeech()
    27}
    28
    29// MARK: TomTomSDKMapDisplay.MapViewDelegate
  4. Extend the MapCoordinator to conform to the MapViewDelegate protocol by implementing the onMapReady callback. The onMapReady callback notifies MapCoordinator that the Map is ready to display. The Map instance can be configured in this callback function:

    1extension MapCoordinator: TomTomSDKMapDisplay.MapViewDelegate {
    2 func mapView(_ mapView: MapView, onMapReady map: TomTomMap) {
    3 // Store the map to be used later
    4 self.map = map
    5
    6 // Observe TomTom map actions
    7 map.delegate = self
    8 // Observe location engine updates
    9 map.locationProvider.addObserver(self)
    10 // Hide the traffic on the map
    11 map.hideTraffic()
    12 // Display a chevron at the current location
    13 map.locationIndicatorType = .navigationChevron(scale: 1)
    14 // Activate the GPS location engine in TomTomSDK.
    15 map.activateLocationProvider()
    16 // Configure the camera to centre on the current location
    17 map.applyCamera(defaultCameraUpdate)
    18 }
    19
    20 func mapView(_ mapView: MapView, onLoadFailed error: Error) {
    21 print("Error occured loading the map \(error.localizedDescription)")
    22 }
    23
    24 func mapView(_ mapView: MapView, onStyleLoad result: Result<StyleContainer, Error>) {
    25 print("Style loaded")
    26 }
    27}
  5. Next create the camera utility functions. The virtual map is observed through a camera that can be zoomed, panned, rotated, and tilted to provide a compelling 3D navigation experience.

    1// MARK: Camera Options
    2
    3extension MapCoordinator {
    4 private var defaultCameraUpdate: CameraUpdate {
    5 let defaultLocation = CLLocation(latitude: 0.0, longitude: 0.0)
    6 return CameraUpdate(
    7 position: defaultLocation.coordinate,
    8 zoom: 1.0,
    9 tilt: 0.0,
    10 rotation: 0.0,
    11 positionMarkerVerticalOffset: 0.0
    12 )
    13 }
    14
    15 func animateCamera(zoom: Double, position: CLLocationCoordinate2D, animationDurationInSeconds: TimeInterval, onceOnly: Bool) {
    16 if onceOnly, cameraUpdated {
    17 return
    18 }
    19 cameraUpdated = true
    20 DispatchQueue.main.async {
    21 var cameraUpdate = self.defaultCameraUpdate
    22 cameraUpdate.zoom = zoom
    23 cameraUpdate.position = position
    24 self.map.applyCamera(cameraUpdate, animationDuration: animationDurationInSeconds)
    25 }
    26 }
    27
    28 func setCamera(trackingMode: TomTomSDKMapDisplay.CameraTrackingMode) {
    29 map?.cameraTrackingMode = trackingMode
    30
    31 // Update chevron height for follow route mode
    32 if trackingMode == .followRoute {
    33 let cameraUpdate = CameraUpdate(positionMarkerVerticalOffset: 0.85)
    34 moveCamera(cameraUpdate: cameraUpdate)
    35 }
    36 }
    37
    38 func moveCamera(cameraUpdate: CameraUpdate) {
    39 map?.moveCamera(cameraUpdate)
    40 }
    41}
    42
    43// MARK: TomTomSDKLocationProvider.LocationProviderObservable

    The remaining steps in this section ensure the project builds without compiler errors.

  6. Extend MapCoordinator to conform to MapDelegate and stub the function to handle a long press on the map:

    1extension MapCoordinator: TomTomSDKMapDisplay.MapDelegate {
    2 func map(_: TomTomSDKMapDisplay.TomTomMap, didLongPressOn coordinate: CLLocationCoordinate2D) {
    3 // Handle long press
    4 }
    5}
  7. Extend MapCoordinator to conform to LocationProviderObservable by adding the following functions:

    1extension MapCoordinator: TomTomSDKLocationProvider.LocationProviderObservable {
    2 func onLocationUpdated(location: GeoLocation) {
    3 // Handle location updates
    4 }
    5
    6 func onHeadingUpdate(newHeading: CLHeading, lastLocation: GeoLocation) {
    7 // Handle heading updates
    8 }
    9
    10 func onAuthorizationStatusChanged(isGranted: Bool) {
    11 // Handle authorization changes
    12 }
    13}
  8. Add an empty declaration for the TextToSpeech class.

    1class TextToSpeech {
    2 // Empty
    3}

You can now build and run the app on a device or simulator to display a zoomed-out map. You can interact with the map by dragging, pinching or using a two-fingered rotation gesture.

Showing the current location

This section explains how to update the camera position to show the current location, and mark the current location on the map.

  1. Replace the MapCoordinator extension that conformed to the LocationProviderObservable protocol with the following:

    1extension MapCoordinator: TomTomSDKLocationProvider.LocationProviderObservable {
    2 func onLocationUpdated(location: GeoLocation) {
    3 // Zoom and center the camera on the first location received.
    4 animateCamera(zoom: 9.0, position: location.location.coordinate, animationDurationInSeconds: 1.5, onceOnly: true)
    5 }
    6
    7 func onHeadingUpdate(newHeading: CLHeading, lastLocation: GeoLocation) {
    8 // Handle heading updates
    9 }
    10
    11 func onAuthorizationStatusChanged(isGranted: Bool) {
    12 // Handle authorization changes
    13 }
    14}
    15
    16// MARK: TomTomSDKMapDisplay.MapDelegate

    This extension enables the MapCoordinator to observe GPS updates and authorization changes. This means that when the application starts, the camera position and zoom level are updated in the onLocationUpdated callback function. The user then sees the current location.

  2. Update the project’s info.plist keys to trigger the app to ask the user for location permission on startup. To do this in Xcode, select the projectthe targetInfo and add the following key-value pairs:

    1Key: Privacy - Location Always and When In Use Usage Description
    2Value: Application requires GPS to display your position on map
    3
    4Key: Privacy - Location When In Use Usage Description
    5Value: Application requires GPS to display your position on map
    6
    7Key: Privacy - Location Always Usage Description
    8Value: Application requires GPS to display your position on map
  3. You can experiment with different ways of showing the current location on the map by changing the locationIndicatorType in the onMapReady callback. Activate the location provider to see the current GPS position on the map:

    1extension MapCoordinator: TomTomSDKMapDisplay.MapViewDelegate {
    2 func mapView(_ mapView: MapView, onMapReady map: TomTomMap) {
    3 ...
    4
    5 // Display a chevron at the current location
    6 map.locationIndicatorType = .navigationChevron(scale: 1)
    7 // Activate the GPS location provider in TomTomSDK.
    8 map.activateLocationProvider()
    9 }
    10}

After completing these steps, build the app. The camera will zoom in on your current GPS location on startup. The location itself is indicated by the blue chevron. The map will display in portrait and landscape modes as the phone is rotated.

center

Planning a route

Now we will add the ability to plan a route to a location on the map by responding to the long press gesture.

  1. Update the MapCoordinator extension of TomTomSDKMapDisplay.MapDelegate to plan a route and add it to the map after a long press:

    1extension MapCoordinator: TomTomSDKMapDisplay.MapDelegate {
    2 func map(_: TomTomSDKMapDisplay.TomTomMap, didLongPressOn coordinate: CLLocationCoordinate2D) {
    3 Task { @MainActor in
    4 let routePlan = try await self.planRoute(to: coordinate)
    5 // First remove any old routes
    6 self.map?.removeRoutes()
    7 self.addRouteToMap(route: routePlan.route)
    8 }
    9 }
    10}

    The call to the async planRoute function is wrapped in a Task so that it can be called from the didLongPressOn delegate callback, even though that callback is not async.

  2. Now create a MapCoordinator extension for the route planning functions:

    1// MARK: Route planning
    2
    3extension MapCoordinator {
    4 enum RoutePlanError: Error {
    5 case unknownStartingLocation
    6 case unableToPlanRoute(_ description: String = "")
    7 }
    8
    9 private func createMapRouteOptions(coordinates: [CLLocationCoordinate2D]) -> TomTomSDKMapDisplay.RouteOptions {
    10 var routeOptions = RouteOptions(coordinates: coordinates)
    11 routeOptions.outlineWidth = 1
    12 routeOptions.routeWidth = 5
    13 routeOptions.color = .activeRoute
    14 return routeOptions
    15 }
    16
    17 func createRoutePlanningOptions(
    18 from origin: CLLocationCoordinate2D,
    19 to destination: CLLocationCoordinate2D
    20 )
    21 throws -> TomTomSDKRoutePlanner.RoutePlanningOptions {
    22 let itinerary = Itinerary(
    23 origin: ItineraryPoint(coordinate: origin),
    24 destination: ItineraryPoint(coordinate: destination)
    25 )
    26 let costModel = CostModel(routeType: .fast)
    27
    28 // For voice announcements:
    29 let guidanceOptions = try GuidanceOptions(
    30 instructionType: .tagged,
    31 language: Locale.current,
    32 roadShieldReferences: .all,
    33 announcementPoints: .all,
    34 phoneticsType: .IPA
    35 )
    36
    37 let options = try RoutePlanningOptions(
    38 itinerary: itinerary,
    39 costModel: costModel,
    40 guidanceOptions: guidanceOptions
    41 )
    42 return options
    43 }
    44
    45 func planRoute(to destination: CLLocationCoordinate2D) async throws -> TomTomSDKNavigationEngines.RoutePlan {
    46 guard let currentLocation = map?.locationProvider.location?.location.coordinate else {
    47 throw RoutePlanError.unknownStartingLocation
    48 }
    49 return try await planRoute(from: currentLocation, to: destination)
    50 }
    51
    52 func planRoute(
    53 from origin: CLLocationCoordinate2D,
    54 to destination: CLLocationCoordinate2D
    55 ) async throws
    56 -> TomTomSDKNavigationEngines.RoutePlan {
    57 let routePlanningOptions = try createRoutePlanningOptions(from: origin, to: destination)
    58 let route = try await planRoute(withRoutePlanner: routePlanner, routePlanningOptions: routePlanningOptions)
    59 return TomTomSDKNavigationEngines.RoutePlan(route: route, routingOptions: routePlanningOptions)
    60 }
    61
    62 func planRoute(
    63 withRoutePlanner routePlanner: OnlineRoutePlanner,
    64 routePlanningOptions: RoutePlanningOptions
    65 ) async throws
    66 -> TomTomSDKRoute.Route {
    67 return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<TomTomSDKRoute.Route, Error>) in
    68 routePlanner.planRoute(options: routePlanningOptions, onRouteReady: nil) { result in
    69 switch result {
    70 case let .failure(error):
    71 if let routingError = error as? RoutingError {
    72 print("Error code: \(routingError.code)")
    73 print("Error message: \(String(describing: routingError.errorDescription))")
    74 continuation.resume(throwing: routingError)
    75 return
    76 }
    77 continuation.resume(throwing: error)
    78 case let .success(response):
    79 guard let routes = response.routes else {
    80 continuation.resume(throwing: RoutePlanError.unableToPlanRoute())
    81 return
    82 }
    83 guard let route = routes.first else {
    84 continuation.resume(throwing: RoutePlanError.unableToPlanRoute())
    85 return
    86 }
    87 continuation.resume(returning: route)
    88 }
    89 }
    90 }
    91 }
    92}
  3. Create a MapCoordinator extension to add the planned route to the map:

    1extension MapCoordinator {
    2 func addRouteToMap(route: TomTomSDKRoute.Route) {
    3 // Create the route options from the route geometry and add it to the map
    4 let routeOptions = createMapRouteOptions(coordinates: route.geometry)
    5 _ = try? map?.addRoute(routeOptions)
    6
    7 // Zoom the map to make the route visible
    8 map?.zoomToRoutes(padding: 32)
    9 }
    10}

You can build the app after completing these steps. The app can then plan a route from your current location to a destination selected with a long press on the map.

Route instruction

Starting navigation

This section explains how to start navigating along the planned route and simulate the journey.

  1. Create a MapCoordinator extension that contains the functions to start and configure navigation:

    1// MARK: Navigation
    2
    3extension MapCoordinator {
    4 func createSimulatedLocationProvider(route: TomTomSDKRoute.Route) -> LocationProvider {
    5 let simulatedLocationProvider = SimulatedLocationProvider(delay: Measurement<UnitDuration>(value: 0.2, unit: .seconds))
    6 simulatedLocationProvider.updateCoordinates(route.geometry, interpolate: true)
    7 return simulatedLocationProvider
    8 }
    9
    10 func createNavigation(locationProvider: LocationProvider) -> TomTomSDKNavigation.Navigation {
    11 let navigationConfiguration = NavigationConfiguration(
    12 apiKey: Keys.ttServicesAPIKey,
    13 locationProvider: locationProvider,
    14 routeReplanner: DefaultRouteReplanner(routePlanner: routePlanner, replanningPolicy: .findBetter),
    15 continuousReplanningMode: .automatic
    16 )
    17 let navigation = Navigation(configuration: navigationConfiguration)
    18 // Assign the MapCoordinator as respective observers to navigation in order to receive navigation callbacks about the journey
    19 navigation.addRouteObserver(self)
    20 navigation.addProgressObserver(self)
    21 navigation.addGuidanceObserver(self)
    22 return navigation
    23 }
    24
    25 func startNavigation(
    26 navigation: TomTomSDKNavigation.Navigation,
    27 locationEngine: TomTomSDKLocationProvider.LocationProvider,
    28 routePlan: TomTomSDKNavigationEngines.RoutePlan
    29 ) {
    30 // Set the navigation location engine
    31 navigation.stop()
    32 navigation.locationProvider = locationEngine
    33
    34 let navigationOptions = NavigationOptions(activeRoutePlan: routePlan)
    35 navigation.start(navigationOptions: navigationOptions)
    36
    37 // The map matched location engine snaps to the route
    38 let locationEngine = navigation.mapMatchedLocationProvider
    39 map?.locationProvider = locationEngine
    40
    41 // Set the camera tracking mode to follow the current position
    42 setCamera(trackingMode: .followRoute)
    43 }
    44}
    45
    46// MARK: TomTomSDKNavigation.NavigationRouteObserver
  2. Next update the didLongPressOn callback function from TomTomMapDelegate to plan the route and start navigating:

    1extension MapCoordinator: TomTomSDKMapDisplay.MapDelegate {
    2 func map(_: TomTomSDKMapDisplay.TomTomMap, didLongPressOn coordinate: CLLocationCoordinate2D) {
    3 Task { @MainActor in
    4 do {
    5 // Plan the route and add it to the map
    6 let routePlan = try await planRoute(to: coordinate)
    7 // First remove any old routes
    8 map?.removeRoutes()
    9 addRouteToMap(route: routePlan.route)
    10
    11 // Sart navigation after a short delay so that we can clearly see the transition to the driving view
    12 DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
    13 guard let self = self else {
    14 return
    15 }
    16
    17 // Use real location updates
    18 // let locationProvider = self.map.locationProvider
    19
    20 // Use simulated location updates
    21 let locationEngine = self.createSimulatedLocationProvider(route: routePlan.route)
    22
    23 let navigation = self.navigation ?? self.createNavigation(locationProvider: locationEngine)
    24 self.startNavigation(
    25 navigation: navigation,
    26 locationEngine: locationEngine,
    27 routePlan: routePlan
    28 )
    29 self.navigation = navigation
    30 // Start location updates
    31 locationEngine.start()
    32 }
    33 } catch {
    34 print("Error when planning a route: \(error)")
    35 }
    36 }
    37 }
    38
    39 func map(_ map: TomTomMap, onInteraction interaction: MapInteraction) {
    40 // Handle gesture
    41 }
    42
    43 func map(_: TomTomSDKMapDisplay.TomTomMap, didChangeCameraTrackingMode mode: CameraTrackingMode) {
    44 // Handle camera tracking mode change
    45 }
    46
    47 func mapDidTapOnRecenterButton(_ map: TomTomSDKMapDisplay.TomTomMap) {
    48 // Handle recenter map
    49 }
    50}

    The didLongPressOn callback function starts navigation by calling startNavigation with the desired location provider. Experiment by switching between real and simulated GPS location providers as shown in the code snippet above.

  3. Extend MapCoordinator to conform to the entire NavigationObserver protocol or to conform to certain protocols which composes NavigationObserver protocol, depend on your needs. The observers are set in the createNavigation function.

    1extension MapCoordinator: TomTomSDKNavigation.NavigationRouteObserver {
    2 func didDeviateFromRoute(currentRoute: TomTomSDKRoute.Route, location: GeoLocation) {}
    3
    4 func didProposeRoutePlan(routePlan: RoutePlan, reason: RouteReplanningReason) {}
    5
    6 func didReplanRoute(replannedRoute: TomTomSDKRoute.Route, reason: RouteReplanningReason) {
    7 print("replanned route")
    8 }
    9
    10 func didReplanRouteOnLanguageChange(replannedRoute: TomTomSDKRoute.Route, reason: RouteReplanningReason, language: Locale) {
    11 print("replanned route as language is changed to \(language.identifier).")
    12 }
    13}
    14
    15// MARK: TomTomSDKNavigation.NavigationProgressObserver
    16
    17extension MapCoordinator: TomTomSDKNavigation.NavigationProgressObserver {
    18 func didUpdateProgress(progress: TomTomSDKNavigationEngines.RouteProgress) {
    19 print("distance along route = \(progress.distanceAlongRoute.tt.meters)m")
    20 }
    21}
    22
    23// MARK: TomTomSDKNavigation.NavigationGuidanceObserver
    24
    25extension MapCoordinator: TomTomSDKNavigation.NavigationGuidanceObserver {
    26 func didUpdateInstructions(instructions: [GuidanceInstruction]) {}
    27
    28 func didUpdateDistanceToNextInstruction(
    29 distance: Measurement<UnitLength>,
    30 instructions: [GuidanceInstruction],
    31 currentPhase: InstructionPhase
    32 ) {}
    33
    34 func didGenerateAnnouncement(announcement: GuidanceAnnouncement, shouldPlay: Bool) {
    35 guard shouldPlay else { return }
    36 textToSpeech.play(announcement: announcement)
    37 }
    38
    39 func didStartLaneGuidance(laneGuidance: LaneGuidance) {}
    40
    41 func didEndLaneGuidance(laneGuidance: LaneGuidance) {}
    42}
    43
    44// MARK: - TextToSpeech
  4. Now stub the play function in the TextToSpeech class:

    1class TextToSpeech {
    2 func play(announcement: TomTomSDKNavigation.GuidanceAnnouncement) {
    3 // Do nothing
    4 }
    5}

You can build the app after completing these steps. Do a long press on the map to plan a route to that location and start the navigation simulation.

Voice guidance

This section explains how to add voice instructions to the app.

  1. Implement the TextToSpeech class as follows:

    1class TextToSpeech {
    2 // MARK: Lifecycle
    3
    4 init(languageCode: String) {
    5 let ttsEngine = SystemTextToSpeechEngine(language: languageCode)
    6 self.ttsProvider = TomTomSDKTextToSpeech.makeTextToSpeech(ttsEngine: ttsEngine)
    7 }
    8
    9 convenience init() {
    10 let languageCode = Locale.preferredLanguages.first ?? Locale.current.languageCode ?? "en-GB"
    11 self.init(languageCode: languageCode)
    12 }
    13
    14 // MARK: Internal
    15
    16 func play(announcement: TomTomSDKNavigationEngines.GuidanceAnnouncement) {
    17 guard !announcement.message.isEmpty else { return }
    18 var phonetics = [TomTomSDKTextToSpeechEngine.PhoneticTranscription]()
    19
    20 if let streetPhonetics = parseStreetPhonetics(phonetics: announcement.messagePhonetics) {
    21 phonetics.append(streetPhonetics)
    22 }
    23 if let signpostPhonetics = parseSignpostPhonetics(phonetics: announcement.messagePhonetics) {
    24 phonetics.append(signpostPhonetics)
    25 }
    26 if let roadNumberPhonetics = parseRoadNumberPhonetics(phonetics: announcement.messagePhonetics) {
    27 phonetics.append(roadNumberPhonetics)
    28 }
    29 let ttsMessage = TTSMessage.tagged(message: announcement.message, phonetics: phonetics)
    30 let priority = TTSMessagePriority(timeout: messageTimeout)
    31 ttsProvider.play(message: ttsMessage, priority: priority)
    32 }
    33
    34 // MARK: Private
    35
    36 private let messageTimeout: TimeInterval = 10.0 // seconds
    37 private let ttsProvider: TomTomSDKTextToSpeech.TextToSpeech
    38
    39 private func parseStreetPhonetics(phonetics: TomTomSDKRoute.Phonetics?) -> TomTomSDKTextToSpeechEngine
    40 .PhoneticTranscription? {
    41 guard let street = phonetics?.street, let languageCode = phonetics?.streetLanguage else {
    42 return nil
    43 }
    44 return PhoneticTranscription(
    45 transcriptions: [street],
    46 languages: [languageCode],
    47 tag: DefaultTags.streetName,
    48 alphabet: DefaultPhoneticAlphabets.ipa
    49 )
    50 }
    51
    52 private func parseSignpostPhonetics(phonetics: TomTomSDKRoute.Phonetics?) -> TomTomSDKTextToSpeechEngine
    53 .PhoneticTranscription? {
    54 guard let signpost = phonetics?.signpostText, let languageCode = phonetics?.signpostTextLanguage else {
    55 return nil
    56 }
    57 return PhoneticTranscription(
    58 transcriptions: [signpost],
    59 languages: [languageCode],
    60 tag: DefaultTags.signpost,
    61 alphabet: DefaultPhoneticAlphabets.ipa
    62 )
    63 }
    64
    65 private func parseRoadNumberPhonetics(phonetics: TomTomSDKRoute.Phonetics?) -> TomTomSDKTextToSpeechEngine
    66 .PhoneticTranscription? {
    67 guard let roadNumbers = phonetics?.roadNumbers, let languages = phonetics?.roadNumbersLanguages else {
    68 return nil
    69 }
    70 return PhoneticTranscription(
    71 transcriptions: roadNumbers,
    72 languages: languages,
    73 tag: DefaultTags.streetName,
    74 alphabet: DefaultPhoneticAlphabets.ipa
    75 )
    76 }
    77}

    The TextToSpeech class configures a text to speech provider using the SystemTextToSpeechEngine. Phonetic data is parsed from the GuidanceAnnouncement object and played back by the TTS provider. The TextToSpeechEngine protocol also declares functions for stopping playback, changing volume, and changing language.

You can build the app after completing these steps. You will be able to hear voice guidance during the navigation simulation.

Next steps

The TomTom Navigation SDK allows you to customize the appearance of the map and its overlays, use your own Navigation UI components, or provide a custom implementation of certain navigation behaviors.