Build a navigation app

VERSION 0.28.2
PUBLIC PREVIEW

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

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

The application displays a map that shows the user’s location and configures the components needed for online navigation. After the user selects a destination with a long click, the app plans a route and draws it on the map. Navigation is started automatically using the route simulation.

Project setup

  1. Create an empty SwiftUI application project with Xcode 13.4 or newer. Set the deployment target to iOS 13.
  2. Complete the project setup guide for the project you’ve created.
  3. Open your project’s Podfile and add the required modules to the project’s target:
    1TOMTOM_SDK_VERSION = '0.28.2'
    2
    3target 'YourAppTarget' do
    4 use_frameworks!
    5 pod 'TomTomSDKCommon', TOMTOM_SDK_VERSION
    6 pod 'TomTomSDKCommonUI', TOMTOM_SDK_VERSION
    7 pod 'TomTomSDKDefaultTextToSpeech', TOMTOM_SDK_VERSION
    8 pod 'TomTomSDKLocationProvider', TOMTOM_SDK_VERSION
    9 pod 'TomTomSDKMapDisplay', TOMTOM_SDK_VERSION
    10 pod 'TomTomSDKNavigation', TOMTOM_SDK_VERSION
    11 pod 'TomTomSDKNavigationEngines', TOMTOM_SDK_VERSION
    12 pod 'TomTomSDKNavigationOnline', TOMTOM_SDK_VERSION
    13 pod 'TomTomSDKNavigationUI', TOMTOM_SDK_VERSION
    14 pod 'TomTomSDKRoute', TOMTOM_SDK_VERSION
    15 pod 'TomTomSDKRoutePlanner', TOMTOM_SDK_VERSION
    16 pod 'TomTomSDKRoutePlannerOnline', TOMTOM_SDK_VERSION
    17 pod 'TomTomSDKRouteReplannerDefault', TOMTOM_SDK_VERSION
    18 pod 'TomTomSDKMapDisplay', TOMTOM_SDK_VERSION
    19 pod 'TomTomSDKNavigation', TOMTOM_SDK_VERSION
    20 pod 'TomTomSDKSearch', TOMTOM_SDK_VERSION
    21 pod 'TomTomSDKRoutePlannerOnline', TOMTOM_SDK_VERSION
    22end
  4. Install the dependencies by executing the following commands in the project directory:
    pod repo-art update tomtom-sdk-cocoapods
    pod install --repo-update

Creating an iOS application

The code will successfully compile only at the end of each section.

Add all the snippets from this tutorial to a single swift file, for example, your project’s App.swift file.

This section explains how to create an empty SwiftUI application with an iOS 13 deployment target and set the MapsDisplayService key to your TomTom API key.

  1. Create an empty launch screen storyboard file and set it to be the "Launch screen storyboard" in the target’s general settings for the application UI to be rendered full screen.
  2. Import the following frameworks in your project’s App.swift file:
    1// System modules
    2import Combine
    3import CoreLocation
    4import SwiftUI
    5
    6// TomTomSDK modules
    7import TomTomSDKCommon
    8import TomTomSDKCommonUI
    9import TomTomSDKDefaultTextToSpeech
    10import TomTomSDKLocationProvider
    11import TomTomSDKMapDisplay
    12import TomTomSDKNavigation
    13import TomTomSDKNavigationEngines
    14import TomTomSDKNavigationOnline
    15import TomTomSDKNavigationUI
    16import TomTomSDKRoute
    17import TomTomSDKRoutePlanner
    18import TomTomSDKRoutePlannerOnline
    19import TomTomSDKRouteReplannerDefault
  3. Create an empty MainView struct:
    1struct MainView: View {
    2 var body: some View {
    3 ZStack(alignment: .bottom) {
    4 Text("Hello TomTom SDK!")
    5 }
    6 }
    7}
  4. Add the AppDelegate class to present the MainView on devices running iOS 13. Set your TomTom API key to the MapsDisplayService in application(_:, didFinishLaunchingWithOptions:) function:
    1class AppDelegate: NSObject, UIApplicationDelegate {
    2 var window: UIWindow?
    3
    4 func application(
    5 _ application: UIApplication,
    6 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    7 )
    8 -> Bool {
    9 TomTomSDKMapDisplay.MapsDisplayService.apiKey = Keys.ttAPIKey
    10
    11 if #available(iOS 14.0, *) {
    12 // TomTomSDKNavigationUseCaseApp will instantiate MainView()
    13 } else {
    14 let window = UIWindow(frame: UIScreen.main.bounds)
    15 window.rootViewController = UIHostingController(
    16 rootView: MainView()
    17 )
    18 self.window = window
    19 window.makeKeyAndVisible()
    20 }
    21
    22 return true
    23 }
    24}
  5. Add the TomTomSDKNavigationUseCaseApp struct to present the MainView on the devices running iOS 14 and newer:
    1@available(iOS 14.0, *)
    2struct TomTomSDKNavigationUseCaseApp: App {
    3 @UIApplicationDelegateAdaptor(AppDelegate.self)
    4 var appDelegate
    5
    6 var body: some Scene {
    7 WindowGroup {
    8 MainView()
    9 }
    10 }
    11}
  6. Add the TomTomSDKNavigationUseCaseAppWrapper.main() function to define the application’s main function for both iOS 13 and iOS 14+ devices:
    1@main
    2enum TomTomSDKNavigationUseCaseAppWrapper {
    3 static func main() {
    4 if #available(iOS 14.0, *) {
    5 TomTomSDKNavigationUseCaseApp.main()
    6 } else {
    7 UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))
    8 }
    9 }
    10}

Build and run your application. The application screen should have a "Hello TomTom SDK!" text.

Displaying a map

Once the iOS application is created you can initialize and display the map.

  1. Create the MapCoordinator class which facilitates communication from the UIKit to the SwiftUI environment:
    1final class MapCoordinator {
    2 init(mapView: TomTomSDKMapDisplay.MapView) {
    3 self.mapView = mapView
    4 }
    5
    6 private let mapView: TomTomSDKMapDisplay.MapView
    7 private var map: TomTomSDKMapDisplay.TomTomMap?
    8}
  2. Extend the MapCoordinator class to conform to the TomTomSDKMapDisplay.MapDelegate protocol and stub the functions to handle a long press on the map:
    1extension MapCoordinator: TomTomSDKMapDisplay.MapDelegate {
    2 func map(_ map: TomTomMap, onInteraction interaction: MapInteraction) {
    3 // Handle map interactions
    4 }
    5
    6 func map(_ map: TomTomMap, onCameraEvent event: CameraEvent) {
    7 // Handle camera events
    8 }
    9}
  3. Extend the MapCoordinator class to conform to the TomTomSDKMapDisplay.MapViewDelegate protocol by implementing the mapView(_:, onMapReady:) delegate function. This delegate function notifies MapCoordinator that the TomTomMap can be displayed. The TomTomMap instance can be configured in this 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 map events
    7 map.delegate = self
    8 }
    9
    10 func mapView(_ mapView: MapView, onStyleLoad result: Result<StyleContainer, Error>) {
    11 print("Style loaded")
    12 }
    13}
  4. Create the TomTomMapView struct:
    1struct TomTomMapView {
    2 var mapView = TomTomSDKMapDisplay.MapView()
    3}
  5. Extend the TomTomMapView struct to conform to the UIViewRepresentable protocol. This allows you to use the TomTomMapView in SwiftUI:
    1extension TomTomMapView: UIViewRepresentable {
    2 func makeUIView(context: Context) -> TomTomSDKMapDisplay.MapView {
    3 mapView.delegate = context.coordinator
    4 return mapView
    5 }
    6
    7 func updateUIView(_: TomTomSDKMapDisplay.MapView, context: Context) {}
    8
    9 func makeCoordinator() -> MapCoordinator {
    10 MapCoordinator(mapView: mapView)
    11 }
    12}
  6. Present TomTomMapView on the application screen. Find the MainView struct declaration and replace it with the following implementation:
    1struct MainView: View {
    2 var body: some View {
    3 ZStack(alignment: .bottom) {
    4 TomTomMapView()
    5 }
    6 }
    7}

Build and run your application. The application screen should have a globe on it.

Navigation use case - displaying a map

Showing user location

This section explains how to show the user’s location on the map and how to make the camera focus on the user’s location.

  1. In Xcode, update the configuration in the target’s Info tab. Set the "Application requires GPS to display your position on map" string value for the following settings:
    • NSLocationAlwaysUsageDescription
    • NSLocationAlwaysAndWhenInUseUsageDescription
    • NSLocationWhenInUseUsageDescription The application will use these strings when requesting location data access from the user. target info location values
  2. Add the cameraUpdated property to the MapCoordinator class. Find the MapCoordinator class declaration and replace it with:
    1final class MapCoordinator {
    2 init(mapView: TomTomSDKMapDisplay.MapView) {
    3 self.mapView = mapView
    4 }
    5
    6 private let mapView: TomTomSDKMapDisplay.MapView
    7 private var map: TomTomSDKMapDisplay.TomTomMap?
    8 private var cameraUpdated = false
    9}
  3. Extend the MapCoordinator class with the camera utility functions to provide a 3D navigation experience by zooming, panning, rotating, and tilting the map:
    1extension MapCoordinator {
    2 private var defaultCameraUpdate: CameraUpdate {
    3 let defaultLocation = CLLocation(latitude: 0.0, longitude: 0.0)
    4 return CameraUpdate(
    5 position: defaultLocation.coordinate,
    6 zoom: 1.0,
    7 tilt: 0.0,
    8 rotation: 0.0,
    9 positionMarkerVerticalOffset: 0.0
    10 )
    11 }
    12
    13 func animateCamera(zoom: Double, position: CLLocationCoordinate2D, animationDurationInSeconds: TimeInterval, onceOnly: Bool) {
    14 if onceOnly, cameraUpdated {
    15 return
    16 }
    17 cameraUpdated = true
    18 var cameraUpdate = defaultCameraUpdate
    19 cameraUpdate.zoom = zoom
    20 cameraUpdate.position = position
    21 map?.applyCamera(cameraUpdate, animationDuration: animationDurationInSeconds)
    22 }
    23}
  4. Extend the MapCoordinator class to conform to the TomTomSDKLocationProvider.LocationProviderObservable protocol. This extension enables the MapCoordinator to observe GPS updates and authorization changes.
    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}
  5. Activate the GPS location engine and start observing location updates. Find the MapCoordinator conformance to the TomTomSDKMapDisplay.MapViewDelegate protocol and replace it with:
    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 map events
    7 map.delegate = self
    8
    9 // Observe location updates
    10 map.locationProvider.addObserver(self)
    11
    12 // Display a chevron
    13 map.locationIndicatorType = .navigationChevron(scale: 1)
    14
    15 // Activate the GPS location engine in TomTomSDK
    16 map.activateLocationProvider()
    17
    18 // Configure the camera to centre on the current location
    19 map.applyCamera(defaultCameraUpdate)
    20 }
    21
    22 func mapView(_ mapView: MapView, onStyleLoad result: Result<StyleContainer, Error>) {
    23 print("Style loaded")
    24 }
    25}

After completing these steps, build the app. The camera will zoom in on your current location at startup. The location itself is indicated by the blue chevron. Don’t forget to simulate the location if you use the iOS Simulator.

Navigation use case - showing current location

Configuring navigation components

  1. Create the NavigationController class which wraps all the navigation-related TomTomSDK components:
    1final class NavigationController: ObservableObject {
    2 // swiftlint:disable force_try
    3 convenience init() {
    4 let textToSpeech = SystemTextToSpeechEngine()
    5 let routePlanner = TomTomSDKRoutePlannerOnline.OnlineRoutePlanner(apiKey: Keys.ttAPIKey)
    6 let routeReplanner = RouteReplannerFactory.create(routePlanner: routePlanner)
    7 let locationProvider = DefaultCLLocationProvider()
    8 let simulatedLocationProvider = SimulatedLocationProvider(delay: Measurement.tt.seconds(1))
    9 let navigationConfiguration = OnlineTomTomNavigationFactory.Configuration(
    10 locationProvider: simulatedLocationProvider,
    11 routeReplanner: routeReplanner,
    12 apiKey: Keys.ttAPIKey,
    13 betterProposalAcceptanceMode: .automatic
    14 )
    15 let navigation = try! OnlineTomTomNavigationFactory
    16 .create(configuration: navigationConfiguration)
    17 let navigationModel = TomTomSDKNavigationUI.NavigationView.ViewModel(navigation, tts: textToSpeech)
    18
    19 self.init(
    20 locationProvider: locationProvider,
    21 simulatedLocationProvider: simulatedLocationProvider,
    22 routePlanner: routePlanner,
    23 navigation: navigation,
    24 navigationModel: navigationModel
    25 )
    26 }
    27
    28 init(
    29 locationProvider: LocationProvider,
    30 simulatedLocationProvider: SimulatedLocationProvider,
    31 routePlanner: TomTomSDKRoutePlannerOnline.OnlineRoutePlanner,
    32 navigation: TomTomNavigation,
    33 navigationModel: TomTomSDKNavigationUI.NavigationView.ViewModel
    34 ) {
    35 self.locationProvider = locationProvider
    36 self.simulatedLocationProvider = simulatedLocationProvider
    37 self.routePlanner = routePlanner
    38 self.navigation = navigation
    39 self.navigationViewModel = navigationModel
    40
    41 self.navigation.addProgressObserver(self)
    42 locationProvider.start()
    43 }
    44
    45 let locationProvider: LocationProvider
    46 let simulatedLocationProvider: SimulatedLocationProvider
    47 let routePlanner: TomTomSDKRoutePlannerOnline.OnlineRoutePlanner
    48 let navigation: TomTomNavigation
    49 let navigationViewModel: TomTomSDKNavigationUI.NavigationView.ViewModel
    50
    51 let displayedRouteSubject = PassthroughSubject<TomTomSDKRoute.Route?, Never>()
    52 let progressOnRouteSubject = PassthroughSubject<Measurement<UnitLength>, Never>()
    53 let mapMatchedLocationProvider = PassthroughSubject<LocationProvider, Never>()
    54
    55 @Published
    56 var showNavigationView: Bool = false
    57}
  2. Conform the NavigationController class to the TomTomSDKNavigation.NavigationProgressObserver protocol to receive route progress updates:
    1extension NavigationController: TomTomSDKNavigation.NavigationProgressObserver {
    2 func didUpdateProgress(progress: RouteProgress) {
    3 progressOnRouteSubject.send(progress.distanceAlongRoute)
    4 }
    5}
  3. Add the navigationController property to the MapCoordinator class. Find the MapCoordinator class declaration and replace it with:
    1final class MapCoordinator {
    2 init(mapView: TomTomSDKMapDisplay.MapView, navigationController: NavigationController) {
    3 self.mapView = mapView
    4 self.navigationController = navigationController
    5 }
    6
    7 private let mapView: TomTomSDKMapDisplay.MapView
    8 private var map: TomTomSDKMapDisplay.TomTomMap?
    9 private var cameraUpdated = false
    10 private let navigationController: NavigationController
    11}
  4. Add the navigationController property to the TomTomMapView struct. Find the TomTomMapView struct declaration and replace it with:
    1struct TomTomMapView {
    2 var mapView = TomTomSDKMapDisplay.MapView()
    3 var navigationController: NavigationController
    4}
  5. Pass the navigationController property to MapCoordinator on initialization. Find the TomTomMapView extension with UIViewRepresentable protocol conformance and replace it with:
    1extension TomTomMapView: UIViewRepresentable {
    2 func makeUIView(context: Context) -> TomTomSDKMapDisplay.MapView {
    3 mapView.delegate = context.coordinator
    4 return mapView
    5 }
    6
    7 func updateUIView(_: TomTomSDKMapDisplay.MapView, context: Context) {}
    8
    9 func makeCoordinator() -> MapCoordinator {
    10 MapCoordinator(mapView: mapView, navigationController: navigationController)
    11 }
    12}
  6. Add the navigationController observable object property to MainView. Find the MainView declaration and replace it with:
    1struct MainView: View {
    2 @ObservedObject
    3 var navigationController = NavigationController()
    4
    5 var body: some View {
    6 ZStack(alignment: .bottom) {
    7 TomTomMapView(navigationController: navigationController)
    8 }
    9 }
    10}

Build and run your application.

Planning a route and starting navigation

Now we will add the ability to plan a route to a location on the map once the long press event has been triggered. Navigation will be started automatically using the route simulation.

  1. Extend the NavigationController class with the route planning functions:

    1extension NavigationController {
    2 enum RoutePlanError: Error {
    3 case unknownStartingLocation
    4 case unableToPlanRoute(_ description: String = "")
    5 }
    6
    7 private func startCoordinate() throws -> CLLocationCoordinate2D {
    8 if let simulatedPosition = simulatedLocationProvider.location?.location.coordinate {
    9 return simulatedPosition
    10 }
    11 if let currentPosition = locationProvider.location?.location.coordinate {
    12 return currentPosition
    13 }
    14 throw RoutePlanError.unknownStartingLocation
    15 }
    16
    17 private 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 let locale = Locale(identifier: "en-GB")
    29 let guidanceOptions = try GuidanceOptions(
    30 instructionType: .tagged,
    31 language: locale,
    32 roadShieldReferences: .all,
    33 announcementPoints: .all,
    34 phoneticsType: .IPA,
    35 progressPoints: .all
    36 )
    37
    38 let options = try RoutePlanningOptions(
    39 itinerary: itinerary,
    40 costModel: costModel,
    41 guidanceOptions: guidanceOptions
    42 )
    43 return options
    44 }
    45
    46 private func planRoute(
    47 from origin: CLLocationCoordinate2D,
    48 to destination: CLLocationCoordinate2D
    49 ) async throws
    50 -> TomTomSDKNavigationEngines.RoutePlan {
    51 let routePlanningOptions = try createRoutePlanningOptions(from: origin, to: destination)
    52 let route = try await planRoute(withRoutePlanner: routePlanner, routePlanningOptions: routePlanningOptions)
    53 return TomTomSDKNavigationEngines.RoutePlan(route: route, routingOptions: routePlanningOptions)
    54 }
    55
    56 private func planRoute(
    57 withRoutePlanner routePlanner: OnlineRoutePlanner,
    58 routePlanningOptions: RoutePlanningOptions
    59 ) async throws
    60 -> TomTomSDKRoute.Route {
    61 return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<TomTomSDKRoute.Route, Error>) in
    62 routePlanner.planRoute(options: routePlanningOptions, onRouteReady: nil) { result in
    63 switch result {
    64 case let .failure(error):
    65 if let routingError = error as? RoutingError {
    66 print("Error code: \(routingError.code)")
    67 print("Error message: \(String(describing: routingError.errorDescription))")
    68 continuation.resume(throwing: routingError)
    69 return
    70 }
    71 continuation.resume(throwing: error)
    72 case let .success(response):
    73 guard let routes = response.routes else {
    74 continuation.resume(throwing: RoutePlanError.unableToPlanRoute())
    75 return
    76 }
    77 guard let route = routes.first else {
    78 continuation.resume(throwing: RoutePlanError.unableToPlanRoute())
    79 return
    80 }
    81 continuation.resume(returning: route)
    82 }
    83 }
    84 }
    85 }
    86}
  2. Extend the NavigationController class with the navigateToCoordinate(destination:) function that plans a route, starts location simulation, and starts the navigation process.

    1extension NavigationController {
    2 func navigateToCoordinate(destination: CLLocationCoordinate2D) {
    3 Task { @MainActor in
    4 do {
    5 // Plan the route and add it to the map
    6 let start = try startCoordinate()
    7 let routePlan = try await planRoute(from: start, to: destination)
    8
    9 stopNavigating()
    10
    11 let route = routePlan.route
    12 self.displayedRouteSubject.send(route)
    13
    14 let navigationOptions = NavigationOptions(activeRoutePlan: routePlan)
    15 self.navigationViewModel.start(navigationOptions)
    16
    17 // Start navigation after a short delay so that we can clearly see the transition to the driving view
    18 try await Task.sleep(nanoseconds: UInt64(1.0 * 1_000_000_000))
    19
    20 // Use simulated location updates
    21 self.simulatedLocationProvider.updateCoordinates(route.geometry, interpolate: true)
    22 self.simulatedLocationProvider.start()
    23 self.mapMatchedLocationProvider.send(navigation.mapMatchedLocationProvider)
    24
    25 self.showNavigationView = true
    26 } catch {
    27 print("Error when planning a route: \(error)")
    28 }
    29 }
    30 }
    31
    32 func stopNavigating() {
    33 displayedRouteSubject.send(nil)
    34 navigationViewModel.stop()
    35 simulatedLocationProvider.stop()
    36 showNavigationView = false
    37 }
    38}

    The call to the async planRoute function is wrapped in a Task so that it can be called from the navigateToCoordinate(destination:) function, even though that function is not async.

  3. Update the implementation of the TomTomSDKMapDisplay.MapDelegate protocol to plan a route and show it on the map after a long press. Find the implementation of the TomTomSDKMapDisplay.MapDelegate protocol and replace it with:

    1extension MapCoordinator: TomTomSDKMapDisplay.MapDelegate {
    2 func map(_ map: TomTomMap, onInteraction interaction: MapInteraction) {
    3 switch interaction {
    4 case let .longPressed(coordinate):
    5 navigationController.navigateToCoordinate(destination: coordinate)
    6 default:
    7 // Handle other gestures
    8 break
    9 }
    10 }
    11
    12 func map(_ map: TomTomMap, onCameraEvent event: CameraEvent) {
    13 // Handle camera events
    14 }
    15}
  4. Add the routeOnMap and cancellableBag properties, and call the observe(navigationController:) function in the MapCoordinator class. Find the MapCoordinator class declaration and replace it with the following implementation:

    1final class MapCoordinator {
    2 init(mapView: TomTomSDKMapDisplay.MapView, navigationController: NavigationController) {
    3 self.mapView = mapView
    4 self.navigationController = navigationController
    5 observe(navigationController: navigationController)
    6 }
    7
    8 private let mapView: TomTomSDKMapDisplay.MapView
    9 private var map: TomTomSDKMapDisplay.TomTomMap?
    10 private var cameraUpdated = false
    11 private let navigationController: NavigationController
    12 private var routeOnMap: TomTomSDKMapDisplay.Route?
    13 private var cancellableBag = Set<AnyCancellable>()
    14}
  5. Extend the MapCoordinator class to add the planned route to the map. After adding a route on the map, you will receive a reference to that route. The addRouteToMap(route:) function stores the reference to a route in the routeOnMap property so that it can show the visual progress along the route.

    1extension MapCoordinator {
    2 private func createMapRouteOptions(coordinates: [CLLocationCoordinate2D]) -> TomTomSDKMapDisplay.RouteOptions {
    3 var routeOptions = RouteOptions(coordinates: coordinates)
    4 routeOptions.outlineWidth = 1
    5 routeOptions.routeWidth = 5
    6 routeOptions.color = .activeRoute
    7 return routeOptions
    8 }
    9
    10 func addRouteToMap(route: TomTomSDKRoute.Route) {
    11 // Create the route options from the route geometry and add it to the map
    12 let routeOptions = createMapRouteOptions(coordinates: route.geometry)
    13 if let routeOnMap = try? map?.addRoute(routeOptions) {
    14 self.routeOnMap = routeOnMap
    15
    16 // Zoom the map to make the route visible
    17 map?.zoomToRoutes(padding: 32)
    18 }
    19 }
    20}
  6. Extend the MapCoordinator class with the setCamera(trackingMode:) function:

    1extension MapCoordinator {
    2 func setCamera(trackingMode: TomTomSDKMapDisplay.CameraTrackingMode) {
    3 map?.cameraTrackingMode = trackingMode
    4
    5 // Update chevron position on the screen so it is not hidden behind the navigation panel
    6 if trackingMode == .followRoute || trackingMode == .follow {
    7 let cameraUpdate = CameraUpdate(positionMarkerVerticalOffset: 0.4)
    8 map?.moveCamera(cameraUpdate)
    9 }
    10 }
    11}
  7. Extend the MapCoordinator class with the observe(navigationController:) function to display changes to the current route and its progress on the map:

    1extension MapCoordinator {
    2 func observe(navigationController: NavigationController) {
    3 navigationController.displayedRouteSubject.sink { [weak self] route in
    4 guard let self = self else { return }
    5 if let route = route {
    6 self.addRouteToMap(route: route)
    7 self.setCamera(trackingMode: .followRoute)
    8 } else {
    9 self.routeOnMap = nil
    10 self.map?.removeRoutes()
    11 self.setCamera(trackingMode: .follow)
    12 }
    13 }.store(in: &cancellableBag)
    14
    15 navigationController.progressOnRouteSubject.sink { [weak self] progress in
    16 self?.routeOnMap?.progressOnRoute = progress
    17 }.store(in: &cancellableBag)
    18
    19 navigationController.mapMatchedLocationProvider.sink { [weak self] locationProvider in
    20 self?.map?.locationProvider = locationProvider
    21 }.store(in: &cancellableBag)
    22 }
    23}
  8. Extend the NavigationController class with navigation events handling. Make sure to use TomTomSDKNavigationUI.NavigationView.ViewModel to display both the visual and voice instructions.

    1extension NavigationController {
    2 func onNavigationViewAction(_ action: TomTomSDKNavigationUI.NavigationView.Action) {
    3 switch action {
    4 case let .arrival(action):
    5 onArrivalAction(action)
    6 case let .instruction(action):
    7 onInstructionAction(action)
    8 case let .confirmation(action):
    9 onConfirmationAction(action)
    10 case let .error(action):
    11 onErrorAction(action)
    12 @unknown default:
    13 /* YOUR CODE GOES HERE */
    14 break
    15 }
    16 }
    17
    18 fileprivate func onArrivalAction(_ action: TomTomSDKNavigationUI.NavigationView.ArrivalAction) {
    19 switch action {
    20 case .close:
    21 stopNavigating()
    22 @unknown default:
    23 /* YOUR CODE GOES HERE */
    24 break
    25 }
    26 }
    27
    28 fileprivate func onInstructionAction(_ action: TomTomSDKNavigationUI.NavigationView.InstructionAction) {
    29 switch action {
    30 case let .tapSound(muted):
    31 navigationViewModel.muteTextToSpeech(mute: !muted)
    32 case .tapLanes:
    33 navigationViewModel.hideLanes()
    34 case .tapThen:
    35 navigationViewModel.hideCombinedInstruction()
    36 @unknown default:
    37 /* YOUR CODE GOES HERE */
    38 break
    39 }
    40 }
    41
    42 fileprivate func onConfirmationAction(_ action: TomTomSDKNavigationUI.NavigationView.ConfirmationAction) {
    43 switch action {
    44 case .yes:
    45 stopNavigating()
    46 case .no:
    47 /* YOUR CODE GOES HERE */
    48 break
    49 @unknown default:
    50 /* YOUR CODE GOES HERE */
    51 break
    52 }
    53 }
    54
    55 fileprivate func onErrorAction(_ action: TomTomSDKNavigationUI.NavigationView.ErrorAction) {
    56 /* YOUR CODE GOES HERE */
    57 }
    58}
  9. Present the Navigation view. Find the MainView struct and replace it with the following implementation:

    1struct MainView: View {
    2 @ObservedObject
    3 var navigationController = NavigationController()
    4
    5 var body: some View {
    6 ZStack(alignment: .bottom) {
    7 TomTomMapView(navigationController: navigationController)
    8 if navigationController.showNavigationView {
    9 NavigationView(
    10 navigationController.navigationViewModel,
    11 action: navigationController.onNavigationViewAction
    12 )
    13 }
    14 }
    15 }
    16}
  10. To enable voice guidance on the iOS 16+ simulator, access the "Settings" app on the simulator’s home screen. Within the Settings, navigate to "Accessibility" → "Spoken Content" and enable the "Speak Selection" toggle. Proceed to the "Voices" submenu, select and download a voice. Finally, press the play button on the downloaded voice to test it.

You can build the app after completing these steps. Do a long press on the map. The app will plan a route from your current location to the selected point on the map and start the navigation simulation, including voice guidance.

Navigation use case - planning a route

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. See the following guides for more information: