Building a navigation app

VERSION 0.45.0
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 14 or newer. Set the deployment target to iOS 14.
  2. Complete the project setup guide for the project you’ve created.
  3. Import the necessary frameworks using the following instructions, based on your preferred package manager:
Swift Package Manager
  1. Open your App’s target and navigate to General > Frameworks, Libraries, and Embedded Content.
  2. Add the following TomTomSDK libraries from the provided code snippet. Once the project is set up, import the mentioned frameworks into your code.
1// System modules.
2import Combine
3import CoreLocation
4import SwiftUI
5
6// TomTomSDK modules.
7import TomTomSDKDefaultTextToSpeech
8import TomTomSDKLocationProvider
9import TomTomSDKMapDisplay
10import TomTomSDKNavigation
11import TomTomSDKNavigationEngines
12import TomTomSDKNavigationOnline
13import TomTomSDKNavigationUI
14import TomTomSDKRoute
15import TomTomSDKRoutePlanner
16import TomTomSDKRoutePlannerOnline
17import TomTomSDKRouteReplannerDefault
18import TomTomSDKRoutingCommon
CocoaPods
  1. Open your project’s Podfile and add the required modules to the project’s target:
    1TOMTOM_SDK_VERSION = '0.45.0'
    2target 'YourAppTarget' do
    3 use_frameworks!
    4 pod 'TomTomSDKCommon', TOMTOM_SDK_VERSION
    5 pod 'TomTomSDKCommonUI', TOMTOM_SDK_VERSION
    6 pod 'TomTomSDKDefaultTextToSpeech', TOMTOM_SDK_VERSION
    7 pod 'TomTomSDKLocationProvider', TOMTOM_SDK_VERSION
    8 pod 'TomTomSDKMapDisplay', TOMTOM_SDK_VERSION
    9 pod 'TomTomSDKNavigation', TOMTOM_SDK_VERSION
    10 pod 'TomTomSDKNavigationEngines', TOMTOM_SDK_VERSION
    11 pod 'TomTomSDKNavigationOnline', TOMTOM_SDK_VERSION
    12 pod 'TomTomSDKNavigationUI', TOMTOM_SDK_VERSION
    13 pod 'TomTomSDKRoute', TOMTOM_SDK_VERSION
    14 pod 'TomTomSDKRoutePlanner', TOMTOM_SDK_VERSION
    15 pod 'TomTomSDKRoutePlannerOnline', TOMTOM_SDK_VERSION
    16 pod 'TomTomSDKRouteReplannerDefault', TOMTOM_SDK_VERSION
    17 pod 'TomTomSDKSearch', TOMTOM_SDK_VERSION
    18end
  2. Install the dependencies by executing the following commands in the project directory:
    pod repo-art update tomtom-sdk-cocoapods
    pod install --repo-update
  3. Import the following frameworks in your project’s <ProjectName>App.swift file:
    1// System modules.
    2import Combine
    3import CoreLocation
    4import SwiftUI
    5
    6// TomTomSDK modules.
    7import TomTomSDKDefaultTextToSpeech
    8import TomTomSDKLocationProvider
    9import TomTomSDKMapDisplay
    10import TomTomSDKNavigation
    11import TomTomSDKNavigationEngines
    12import TomTomSDKNavigationOnline
    13import TomTomSDKNavigationUI
    14import TomTomSDKRoute
    15import TomTomSDKRoutePlanner
    16import TomTomSDKRoutePlannerOnline
    17import TomTomSDKRouteReplannerDefault
    18import TomTomSDKRoutingCommon

With the dependencies set up, you can proceed to the next step and start displaying the map.

Combine all the code snippets from this tutorial into a single Swift file, such as your project’s 'App.swift' file. The code only successfully compiles at the end of each section.

Displaying a map

After successfully creating your iOS application, you can initialize and display the map with the following steps:

Step 1: Assigning the API Key

Assign the key for TomTomSDKMapDisplay by adding the following line to your app’s initializer method:

1@main
2struct MyNavigationApp: App {
3 var body: some Scene {
4 WindowGroup {
5 ContentView()
6 .ignoresSafeArea(edges: [.top, .bottom])
7 }
8 }
9
10 init() {
11 TomTomSDKMapDisplay.MapsDisplayService.apiKey = Keys.ttAPIKey
12 }
13}

Step 2: Map UI setup

Construct the MapCoordinator class that is used to handle map events:

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}

Construct the TomTomMapView structure, which contains the map view setup:

1struct TomTomMapView {
2 var mapView = TomTomSDKMapDisplay.MapView()
3}

Step 3: SwiftUI integration

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 return mapView
4 }
5
6 func updateUIView(_: TomTomSDKMapDisplay.MapView, context: Context) {}
7
8 func makeCoordinator() -> MapCoordinator {
9 MapCoordinator(mapView: mapView)
10 }
11}

Step 4: Displaying the map

Replace the ContentView structure declaration, enabling the presentation of the TomTomMapView on the application screen:

1struct ContentView: View {
2 var body: some View {
3 ZStack(alignment: .bottom) {
4 TomTomMapView()
5 }
6 }
7}

Once you have the TomTomMap object, you can perform various actions such as adding markers or drawing routes.

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

Navigation use case - displaying a map

Showing user location

This section guides you through displaying the user’s location on the map and adjusting the camera to focus on the user’s position. To enable this functionality, use the following steps:

Step 1: Enabling location services

Before proceeding with code changes, you must properly configure your app’s Info.plist according to the Apple documentation, for the following keys:

  • NSLocationAlwaysUsageDescription
  • NSLocationAlwaysAndWhenInUseUsageDescription
  • NSLocationWhenInUseUsageDescription

Step 2: Setting up map coordinator

Add the cameraUpdated parameter to MapCoordinator to handle camera updates:

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}

Step 3: Enabling camera utility functions

Extend the MapCoordinator class with utility functions that control the camera’s behavior. These functions enable: * zooming, * panning, * rotating, and * tilting the map. More information about camera controls on CameraUpdate:

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(
14 zoom: Double,
15 position: CLLocationCoordinate2D,
16 animationDurationInSeconds: TimeInterval,
17 onceOnly: Bool
18 ) {
19 if onceOnly, cameraUpdated {
20 return
21 }
22 cameraUpdated = true
23 var cameraUpdate = defaultCameraUpdate
24 cameraUpdate.zoom = zoom
25 cameraUpdate.position = position
26 map?.applyCamera(
27 cameraUpdate,
28 animationDuration: animationDurationInSeconds
29 )
30 }
31}

Step 4: Following user location

Extend the MapCoordinator class to conform to the TomTomSDKLocationProvider.LocationProviderObservable protocol 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(
5 zoom: 9.0,
6 position: location.location.coordinate,
7 animationDurationInSeconds: 1.5,
8 onceOnly: true
9 )
10 }
11
12 func onHeadingUpdate(newHeading: CLHeading, lastLocation: GeoLocation) {
13 // Handle heading updates.
14 }
15
16 func onAuthorizationStatusChanged(isGranted: Bool) {
17 // Handle authorization changes.
18 }
19}

Step 5: Starting location updates

Add an extension to MapCoordinator that conforms to TomTomSDKMapDisplay.MapDelegate:

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}

In your MapCoordinator conform to the TomTomSDKMapDisplay.MapViewDelegate protocol, update the MapViewDelegate.mapView(_:onMapReady:) method to start observing location updates:

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 center on the current location.
19 map.applyCamera(defaultCameraUpdate)
20 }
21
22 func mapView(
23 _ mapView: MapView,
24 onStyleLoad result: Result<StyleContainer, Error>
25 ) {
26 print("Style loaded")
27 }
28}

Finally, find the TomTomMapView extension that conforms to UIViewRepresentable and set the MapView delegate:

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}

Once you’ve completed these steps, build your app. The camera will now zoom in on the user’s current location upon startup, indicated by the blue chevron. Remember to simulate the location if you’re using the iOS Simulator for testing.

Navigation use case - showing current location

Planning a route

Now you can add the ability to plan a route to a location on the map once the long press event has been triggered.

Step 1: Setting up the route planner

Create a new class, NavigationController, with OnlineRoutePlanner to handle route planning:

1final class NavigationController: ObservableObject {
2 convenience init() {
3 let routePlanner = TomTomSDKRoutePlannerOnline
4 .OnlineRoutePlanner(apiKey: Keys.ttAPIKey)
5 let locationProvider = DefaultCLLocationProvider()
6
7 self.init(
8 locationProvider: locationProvider,
9 routePlanner: routePlanner
10 )
11 }
12
13 init(
14 locationProvider: LocationProvider,
15 routePlanner: TomTomSDKRoutePlannerOnline.OnlineRoutePlanner
16 ) {
17 self.locationProvider = locationProvider
18 self.routePlanner = routePlanner
19 self.locationProvider.start()
20 }
21
22 let locationProvider: LocationProvider
23 let routePlanner: TomTomSDKRoutePlannerOnline.OnlineRoutePlanner
24
25 var routePlanningOptions: RoutePlanningOptions?
26
27 let displayedRouteSubject = PassthroughSubject<
28 TomTomSDKRoute.Route?,
29 Never
30 >()
31}

Update your TomTomMapView by adding a NavigationController:

1struct TomTomMapView {
2 var mapView = TomTomSDKMapDisplay.MapView()
3 var navigationController: NavigationController
4}

Update the TomTomMapView UIViewRepresentable implementation:

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(
11 mapView: mapView,
12 navigationController: navigationController
13 )
14 }
15}

Update the TomTomMapView initializer in your ContentView by adding a NavigationController:

1struct ContentView: View {
2 @ObservedObject
3 var navigationController = NavigationController()
4
5 var body: some View {
6 ZStack(alignment: .bottom) {
7 TomTomMapView(navigationController: navigationController)
8 }
9 }
10}

Step 2: Planning a route

Include the RoutePlanner.planRoute(options:onRouteReady:completion:) method, which calculates a route between specified origin and destination coordinates:

1extension NavigationController {
2 enum RoutePlanError: Error {
3 case unableToPlanRoute
4 }
5
6 private func planRoute(
7 from origin: CLLocationCoordinate2D,
8 to destination: CLLocationCoordinate2D
9 ) async throws
10 -> TomTomSDKNavigationEngines.RoutePlan {
11 routePlanningOptions = try createRoutePlanningOptions(
12 from: origin,
13 to: destination
14 )
15 let route = try await planRoute(
16 withRoutePlanner: routePlanner,
17 routePlanningOptions: routePlanningOptions!
18 )
19 return TomTomSDKNavigationEngines
20 .RoutePlan(route: route, routePlanningOptions: routePlanningOptions!)
21 }
22
23 private func createRoutePlanningOptions(
24 from origin: CLLocationCoordinate2D,
25 to destination: CLLocationCoordinate2D
26 )
27 throws -> TomTomSDKRoutePlanner.RoutePlanningOptions {
28 let itinerary = Itinerary(
29 origin: ItineraryPoint(coordinate: origin),
30 destination: ItineraryPoint(coordinate: destination)
31 )
32 let costModel = CostModel(routeType: .fast)
33
34 let locale = Locale(identifier: "en-GB")
35 let guidanceOptions = try GuidanceOptions(
36 instructionType: .tagged,
37 language: locale,
38 roadShieldReferences: .all,
39 announcementPoints: .all,
40 phoneticsType: .IPA,
41 progressPoints: .all
42 )
43
44 let options = try RoutePlanningOptions(
45 itinerary: itinerary,
46 costModel: costModel,
47 guidanceOptions: guidanceOptions
48 )
49 return options
50 }
51
52 private func planRoute(
53 withRoutePlanner routePlanner: OnlineRoutePlanner,
54 routePlanningOptions: RoutePlanningOptions
55 ) async throws
56 -> TomTomSDKRoute.Route {
57 return try await withCheckedThrowingContinuation {
58 (continuation: CheckedContinuation<TomTomSDKRoute.Route, Error>) in
59
60 routePlanner.planRoute(
61 options: routePlanningOptions,
62 onRouteReady: nil
63 ) { result in
64 switch result {
65 case let .failure(error):
66 continuation.resume(throwing: error)
67 case let .success(response):
68 guard let route = response.routes?.first else {
69 continuation
70 .resume(throwing: RoutePlanError.unableToPlanRoute)
71 return
72 }
73 continuation.resume(returning: route)
74 }
75 }
76 }
77 }
78}

Step 3: Preparing the route to be displayed

Implement the planAndDisplayRoute(destination: CLLocationCoordinate2D) method, which is responsible for planning a route and sending it to the map for display:

1extension NavigationController {
2 func planAndDisplayRoute(destination: CLLocationCoordinate2D) {
3 Task { @MainActor in
4 do {
5 // Plan the route and add it to the map
6 guard let start = locationProvider.location?.location.coordinate
7 else { return }
8 let routePlan = try await planRoute(
9 from: start,
10 to: destination
11 )
12
13 let route = routePlan.route
14 self.displayedRouteSubject.send(route)
15 } catch {
16 print("Error when planning a route: \(error)")
17 }
18 }
19 }
20}

Step 4: Displaying the route on the map

Update `MapCoordinator`s extension to incorporate route planning functionality, allowing it to calculate a route from the current location to the selected destination when a long press event occurs:

1extension MapCoordinator: TomTomSDKMapDisplay.MapDelegate {
2 func map(_ map: TomTomMap, onInteraction interaction: MapInteraction) {
3 switch interaction {
4 case let .longPressed(coordinate):
5 navigationController.planAndDisplayRoute(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}

Extend the MapCoordinator with the MapCoordinator.observe(navigationController:) method that will display the route on the map after it is planned:

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 } else {
8 self.routeOnMap = nil
9 self.map?.removeRoutes()
10 }
11 }.store(in: &cancellableBag)
12 }
13}

Execute the MapCoordinator.observe(navigationController:) method in the MapCoordinator initializer:

1final class MapCoordinator {
2 init(
3 mapView: TomTomSDKMapDisplay.MapView,
4 navigationController: NavigationController
5 ) {
6 self.mapView = mapView
7 self.navigationController = navigationController
8 observe(navigationController: navigationController)
9 }
10
11 private let mapView: TomTomSDKMapDisplay.MapView
12 private var map: TomTomSDKMapDisplay.TomTomMap?
13 private var cameraUpdated = false
14 private let navigationController: NavigationController
15 private var routeOnMap: TomTomSDKMapDisplay.Route?
16 private var cancellableBag = Set<AnyCancellable>()
17}

Extend the functionality of MapCoordinator by adding methods for drawing routes on the map and configuring camera tracking modes:

1extension MapCoordinator {
2 private func createMapRouteOptions(
3 coordinates: [CLLocationCoordinate2D]
4 )
5 -> TomTomSDKMapDisplay.RouteOptions {
6 var routeOptions = RouteOptions(coordinates: coordinates)
7 routeOptions.outlineWidth = 1
8 routeOptions.routeWidth = 5
9 routeOptions.color = .activeRoute
10 return routeOptions
11 }
12
13 func addRouteToMap(route: TomTomSDKRoute.Route) {
14 // Create the route options from the route geometry
15 // and adds it to the map.
16 let routeOptions = createMapRouteOptions(coordinates: route.geometry)
17 if let routeOnMap = try? map?.addRoute(routeOptions) {
18 self.routeOnMap = routeOnMap
19
20 // Zoom the map to make the route visible.
21 map?.zoomToRoutes(padding: 32)
22 }
23 }
24
25 func setCamera(trackingMode: TomTomSDKMapDisplay.CameraTrackingMode) {
26 map?.cameraTrackingMode = trackingMode
27
28 // Update the chevron position on the screen
29 // so it is not hidden behind the navigation panel.
30 if trackingMode == .followRouteDirection() || trackingMode == .followNorthUp() {
31 let cameraUpdate = CameraUpdate(positionMarkerVerticalOffset: 0.4)
32 map?.moveCamera(cameraUpdate)
33 }
34 }
35}

After completing these steps, you can build the application. Just perform a long press on the map, and the application plans a route from your current location to the selected point on the map, displaying it on the screen:

Navigation use case - planning a route

Setting up turn-by-turn navigation

In this section, we show you how to navigate on a planned route.

Step 1: Enable Background App Refresh

Create an Info.plist file with the UIBackgroundModes key, this will allow for map updates in the background:

1<plist version="1.0">
2<dict>
3 <key>UIBackgroundModes</key>
4 <array>
5 <string>fetch</string>
6 </array>
7</dict>
8</plist>

Step 2: Setting up navigation

Add a TomTomNavigation object to the NavigationController to process location updates and convert them into navigation events:

1final class NavigationController: ObservableObject {
2 // swiftlint:disable force_cast force_try
3 convenience init() {
4 let textToSpeech = try! SystemTextToSpeechEngine()
5 let routePlanner = TomTomSDKRoutePlannerOnline
6 .OnlineRoutePlanner(apiKey: Keys.ttAPIKey)
7 let routeReplanner = RouteReplannerFactory
8 .create(routePlanner: routePlanner)
9 let locationProvider = DefaultCLLocationProvider()
10 let delay = Measurement.tt.seconds(1)
11 let simulatedLocationProvider = SimulatedLocationProvider(delay: delay)
12 let navigationConfiguration = OnlineTomTomNavigationFactory
13 .Configuration(
14 locationProvider: simulatedLocationProvider,
15 routeReplanner: routeReplanner,
16 apiKey: Keys.ttAPIKey,
17 betterProposalAcceptanceMode: .automatic
18 )
19 let navigation = try! OnlineTomTomNavigationFactory
20 .create(configuration: navigationConfiguration) as! TomTomNavigation
21 let navigationModel = TomTomSDKNavigationUI.NavigationView
22 .ViewModel(navigation, tts: textToSpeech)
23
24 self.init(
25 locationProvider: locationProvider,
26 simulatedLocationProvider: simulatedLocationProvider,
27 routePlanner: routePlanner,
28 navigation: navigation,
29 navigationModel: navigationModel
30 )
31 }
32
33 init(
34 locationProvider: LocationProvider,
35 simulatedLocationProvider: SimulatedLocationProvider,
36 routePlanner: TomTomSDKRoutePlannerOnline.OnlineRoutePlanner,
37 navigation: TomTomSDKNavigation.TomTomNavigation,
38 navigationModel: TomTomSDKNavigationUI.NavigationView.ViewModel
39 ) {
40 self.locationProvider = locationProvider
41 self.simulatedLocationProvider = simulatedLocationProvider
42 self.routePlanner = routePlanner
43 self.navigation = navigation
44 self.navigationViewModel = navigationModel
45
46 self.navigation.addProgressObserver(self)
47 self.navigation.addRouteAddObserver(self)
48 self.navigation.addRouteRemoveObserver(self)
49 self.navigation.addRouteUpdateObserver(self)
50 self.navigation.addActiveRouteChangeObserver(self)
51 locationProvider.start()
52 }
53
54 let locationProvider: LocationProvider
55 let simulatedLocationProvider: SimulatedLocationProvider
56 let routePlanner: TomTomSDKRoutePlannerOnline.OnlineRoutePlanner
57 let navigation: TomTomSDKNavigation.TomTomNavigation
58 let navigationViewModel: TomTomSDKNavigationUI.NavigationView.ViewModel
59
60 var routePlanningOptions: RoutePlanningOptions?
61 var displayedRoutes: [UUID: TomTomSDKRoute.Route] = [:]
62
63 let displayedRouteSubject = PassthroughSubject<
64 TomTomSDKRoute.Route?,
65 Never
66 >()
67 let navigateRouteSubject = PassthroughSubject<
68 TomTomSDKRoute.Route?,
69 Never
70 >()
71 let progressOnRouteSubject = PassthroughSubject<
72 Measurement<UnitLength>,
73 Never
74 >()
75 let mapMatchedLocationProvider = PassthroughSubject<
76 LocationProvider,
77 Never
78 >()
79
80 @Published
81 var showNavigationView: Bool = false
82}

Extend the NavigationController with a navigateOn(route: TomTomSDKRoute.Route) method. It will start navigation on the selected route:

1extension NavigationController {
2 func navigateOn(route: TomTomSDKRoute.Route) {
3 Task { @MainActor in
4 guard let routePlanningOptions else { return }
5
6 self.displayedRouteSubject.send(nil) // clear previous route after replanning
7
8 self.displayedRouteSubject.send(route)
9
10 let routePlan = RoutePlan(route: route, routePlanningOptions: routePlanningOptions)
11
12 let navigationOptions = NavigationOptions(
13 activeRoutePlan: routePlan
14 )
15 self.navigationViewModel.start(navigationOptions)
16 // Use simulated location updates.
17 self.simulatedLocationProvider
18 .updateCoordinates(route.geometry, interpolate: true)
19 self.simulatedLocationProvider.start()
20 self.mapMatchedLocationProvider
21 .send(navigation.mapMatchedLocationProvider)
22
23 self.navigateRouteSubject.send(route)
24 self.showNavigationView = true
25 }
26 }
27
28 func stopNavigating() {
29 displayedRouteSubject.send(nil)
30 navigationViewModel.stop()
31 simulatedLocationProvider.stop()
32 showNavigationView = false
33 }
34}

Step 3: Starting navigation on the selected route

Extend the NavigationController with RouteDelegate. This lets you know when a user presses on the route:

1extension NavigationController: RouteDelegate {
2 func mapRoute(
3 _ route: TomTomSDKMapDisplay.Route,
4 didTapOnCoordinate coordinate: CLLocationCoordinate2D,
5 waypointIndex: Int
6 ) {
7 guard let route = displayedRoutes[route.routeID]
8 else { return }
9
10 navigateOn(route: route)
11 }
12}

Update the MapCoordinator.addRouteToMap(route:) method to add the RouteDelegate.

1func addRouteToMap(route: TomTomSDKRoute.Route, zoom: Bool = false) {
2 // Create the route options from the route geometry
3 // and add it to the map.
4 let routeOptions = createMapRouteOptions(coordinates: route.geometry)
5 if let routeOnMap = try? map?.addRoute(routeOptions) {
6 self.routeOnMap = routeOnMap
7 navigationController.displayedRoutes[routeOnMap.routeID] = route
8 routeOnMap.delegate = navigationController
9
10 // Zoom the map to make the route visible.
11 if !navigationController.showNavigationView {
12 map?.zoomToRoutes(padding: 32) // zoom to the route if no navigation in progress
13 }
14 }
15}

Step 4: Observing navigation events

Extend NavigationController to conform to NavigationProgressObserver to enable progress updates during navigation:

1extension NavigationController: TomTomSDKNavigation.NavigationProgressObserver {
2 func didUpdateProgress(progress: RouteProgress) {
3 progressOnRouteSubject.send(progress.distanceAlongRoute)
4 }
5}

Extend the NavigationController class to handle navigation events and ensure the use of TomTomSDKNavigationUI.NavigationView.ViewModel for displaying both visual and voice instructions:

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

Extend the NavigationController class to handle the following route related changes:

  • adding of a new route, e.g. if navigation proposes a better route
  • removal of an existing route, e.g. if a better route proposal is rejected
  • updating of an existing route, e.g. to obtain the latest traffic information
  • changing of the active route, e.g. if a better route proposal is accepted
1extension NavigationController: TomTomSDKNavigation.NavigationRouteAddObserver,
2 TomTomSDKNavigation.NavigationRouteRemoveObserver,
3 TomTomSDKNavigation.NavigationActiveRouteChangeObserver,
4 TomTomSDKNavigation.NavigationRouteUpdateObserver {
5 func didAddRoute(
6 route: TomTomSDKRoute.Route,
7 options: TomTomSDKRoutePlanner.RoutePlanningOptions,
8 reason: TomTomSDKNavigation.RouteAddedReason
9 ) {
10 let triggeringReasons: [RouteAddedReason] = [.avoidBlockage, .deviated, .manuallyUpdated, .withinRange]
11 if triggeringReasons.contains(reason) {
12 displayedRouteSubject.send(nil) // clear previous route after replanning
13 displayedRouteSubject.send(route)
14 }
15 }
16
17 func didRemoveRoute(route: TomTomSDKRoute.Route, reason: TomTomSDKNavigation.RouteRemovedReason) {}
18
19 func didChangeActiveRoute(route: TomTomSDKRoute.Route) {}
20
21 func didUpdateRoute(route: TomTomSDKRoute.Route, reason: TomTomSDKNavigation.RouteUpdatedReason) {}
22}

Update the MapCoordinator.observe(navigationController:) method to process progress and LocationProvider updates:

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 } else {
8 self.navigationController.displayedRoutes = [:]
9 self.routeOnMap = nil
10 self.map?.removeRoutes()
11 }
12 }.store(in: &cancellableBag)
13
14 navigationController.navigateRouteSubject
15 .sink { [weak self] route in
16 guard let self = self else { return }
17 if let route = route {
18 self.setCamera(trackingMode: .followRouteDirection())
19 } else {
20 self.setCamera(trackingMode: .followNorthUp())
21 }
22 }.store(in: &cancellableBag)
23
24 navigationController.progressOnRouteSubject
25 .sink { [weak self] progress in
26 self?.routeOnMap?.progressOnRoute = progress
27 }.store(in: &cancellableBag)
28
29 navigationController.mapMatchedLocationProvider
30 .sink { [weak self] locationProvider in
31 self?.map?.locationProvider = locationProvider
32 }.store(in: &cancellableBag)
33 }
34}

Step 5: Displaying navigation UI

Include a NavigationView in your ContentView to enable the display of navigation-related information:

1struct ContentView: 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}

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.

After completing these steps, you can build the app. Simply, perform a long press on the map and 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 lets you 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: