Building a navigation app

The Navigation SDK for iOS is only available upon request. Contact Sales 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.

This tutorial on GitHub

Project setup

  1. Create an empty SwiftUI application project with Xcode 16.0 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.
// System modules.
import Combine
import CoreLocation
import SwiftUI
// TomTomSDK modules.
import TomTomSDKCommonUI
import TomTomSDKDefaultTextToSpeech
import TomTomSDKLocationProvider
import TomTomSDKMapDisplay
import TomTomSDKNavigation
import TomTomSDKNavigationEngines
import TomTomSDKNavigationOnline
import TomTomSDKNavigationTileStore
import TomTomSDKNavigationUI
import TomTomSDKRoute
import TomTomSDKRoutePlanner
import TomTomSDKRoutePlannerOnline
import TomTomSDKRoutingCommon
CocoaPods
  1. Open your project’s Podfile and add the required modules to the project’s target:
    TOMTOM_SDK_VERSION = '0.71.1'
    target 'YourAppTarget' do
    use_frameworks!
    pod 'TomTomSDKCommon', TOMTOM_SDK_VERSION
    pod 'TomTomSDKCommonUI', TOMTOM_SDK_VERSION
    pod 'TomTomSDKDefaultTextToSpeech', TOMTOM_SDK_VERSION
    pod 'TomTomSDKLocationProvider', TOMTOM_SDK_VERSION
    pod 'TomTomSDKMapDisplay', TOMTOM_SDK_VERSION
    pod 'TomTomSDKNavigation', TOMTOM_SDK_VERSION
    pod 'TomTomSDKNavigationEngines', TOMTOM_SDK_VERSION
    pod 'TomTomSDKNavigationOnline', TOMTOM_SDK_VERSION
    pod 'TomTomSDKNavigationUI', TOMTOM_SDK_VERSION
    pod 'TomTomSDKRoute', TOMTOM_SDK_VERSION
    pod 'TomTomSDKRoutePlanner', TOMTOM_SDK_VERSION
    pod 'TomTomSDKRoutePlannerOnline', TOMTOM_SDK_VERSION
    pod 'TomTomSDKRouteReplannerDefault', TOMTOM_SDK_VERSION
    pod 'TomTomSDKSearch', TOMTOM_SDK_VERSION
    end
  2. Install the dependencies by executing the following commands in the project directory:
    pod repo update tomtom-sdk-cocoapods
    pod install --repo-update
  3. Import the following frameworks in your project’s <ProjectName>App.swift file:
    // System modules.
    import Combine
    import CoreLocation
    import SwiftUI
    // TomTomSDK modules.
    import TomTomSDKCommonUI
    import TomTomSDKDefaultTextToSpeech
    import TomTomSDKLocationProvider
    import TomTomSDKMapDisplay
    import TomTomSDKNavigation
    import TomTomSDKNavigationEngines
    import TomTomSDKNavigationOnline
    import TomTomSDKNavigationTileStore
    import TomTomSDKNavigationUI
    import TomTomSDKRoute
    import TomTomSDKRoutePlanner
    import TomTomSDKRoutePlannerOnline
    import 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:

// Replace the content of the App struct in your Xcode project with the content of the provided snippet.
@main
struct MyNavigationApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.ignoresSafeArea(edges: [.top, .bottom])
}
}
init() {
TomTomSDKMapDisplay.MapsDisplayService.apiKey = Keys.tomtomApiKey
}
}

Step 2: Map UI setup

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

final class MapCoordinator {
init(mapView: TomTomSDKMapDisplay.MapView) {
self.mapView = mapView
}
private let mapView: TomTomSDKMapDisplay.MapView
private var map: TomTomSDKMapDisplay.TomTomMap?
}

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

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

Step 3: SwiftUI integration

Extend the TomTomMapView struct to conform to the UIViewRepresentable protocol. This allows you to use the TomTomMapView in SwiftUI:

extension TomTomMapView: UIViewRepresentable {
func makeUIView(context: Context) -> TomTomSDKMapDisplay.MapView {
return mapView
}
func updateUIView(_: TomTomSDKMapDisplay.MapView, context: Context) {}
func makeCoordinator() -> MapCoordinator {
MapCoordinator(mapView: mapView)
}
}

Step 4: Displaying the map

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

struct ContentView: View {
var body: some View {
ZStack(alignment: .bottom) {
TomTomMapView()
}
}
}

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:

  • NSLocationAlwaysAndWhenInUseUsageDescription
  • NSLocationWhenInUseUsageDescription

Step 2: Requesting authorization to use location services

To get appropriate permissions, you must send authorization request by calling requestWhenInUseAuthorization() or requestAlwaysAuthorization() on the CLLocationManager instance respectively. Complete instructions to request location permissions on iOS can be found at Requesting authorization for location services .

Step 3: Setting up map coordinator

Add the cameraUpdated parameter to MapCoordinator to handle camera updates:

final class MapCoordinator {
init(
mapView: TomTomSDKMapDisplay.MapView,
locationManager: CLLocationManager = CLLocationManager()
) {
self.mapView = mapView
self.locationManager = locationManager
locationManager.requestWhenInUseAuthorization()
}
private let locationManager: CLLocationManager
private let mapView: TomTomSDKMapDisplay.MapView
private var map: TomTomSDKMapDisplay.TomTomMap?
private var cameraUpdated = false
}

Step 4: 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 :

extension MapCoordinator {
private var defaultCameraUpdate: CameraUpdate {
let defaultLocation = CLLocation(latitude: 0.0, longitude: 0.0)
return CameraUpdate(
position: defaultLocation.coordinate,
zoom: 1.0,
tilt: 0.0,
rotation: 0.0,
positionMarkerVerticalOffset: 0.0
)
}
func animateCamera(
zoom: Double,
position: CLLocationCoordinate2D,
animationDurationInSeconds: TimeInterval,
onceOnly: Bool
) {
if onceOnly, cameraUpdated {
return
}
cameraUpdated = true
var cameraUpdate = defaultCameraUpdate
cameraUpdate.zoom = zoom
cameraUpdate.position = position
map?.applyCamera(
cameraUpdate,
animationDuration: animationDurationInSeconds
)
}
}

Step 5: Following user location

Extend the MapCoordinator class to conform to the TomTomSDKLocationProvider.LocationUpdateObserver protocol to observe GPS updates:

extension MapCoordinator: TomTomSDKLocationProvider.LocationUpdateObserver {
func didUpdateLocation(location: GeoLocation) {
// Zoom and center the camera on the first location received.
animateCamera(
zoom: 9.0,
position: location.location.coordinate,
animationDurationInSeconds: 1.5,
onceOnly: true
)
}
}

Step 6: Starting location updates

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

extension MapCoordinator: TomTomSDKMapDisplay.MapDelegate {
func map(_ map: TomTomMap, onInteraction interaction: MapInteraction) {
// Handle map interactions.
}
func map(_ map: TomTomMap, onCameraEvent event: CameraEvent) {
// Handle camera events.
}
}

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

extension MapCoordinator: TomTomSDKMapDisplay.MapViewDelegate {
func mapView(_ mapView: MapView, onMapReady map: TomTomMap) {
// Store the map to be used later.
self.map = map
// Observe map events.
map.delegate = self
// Observe location updates.
map.locationProvider.addObserver(self)
// Display a chevron.
map.locationIndicatorType = .navigationChevron(scale: 1)
// Activate the GPS location engine in TomTomSDK.
map.activateLocationProvider()
// Configure the camera to center on the current location.
map.applyCamera(defaultCameraUpdate)
}
func mapView(
_ mapView: MapView,
onStyleLoad result: Result<StyleContainer, Error>
) {
print("Style loaded")
}
}

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

extension TomTomMapView: UIViewRepresentable {
func makeUIView(context: Context) -> TomTomSDKMapDisplay.MapView {
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_: TomTomSDKMapDisplay.MapView, context: Context) {}
func makeCoordinator() -> MapCoordinator {
MapCoordinator(mapView: mapView)
}
}

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:

final class NavigationController: ObservableObject {
convenience init() {
let routePlanner = TomTomSDKRoutePlannerOnline
.OnlineRoutePlanner(apiKey: Keys.tomtomApiKey)
let locationProvider = DefaultCLLocationProvider()
self.init(
locationProvider: locationProvider,
routePlanner: routePlanner,
locationManager: CLLocationManager()
)
}
init(
locationProvider: LocationProvider,
routePlanner: TomTomSDKRoutePlannerOnline.OnlineRoutePlanner,
locationManager: CLLocationManager
) {
self.locationProvider = locationProvider
self.routePlanner = routePlanner
self.locationManager = locationManager
self.locationManager.requestWhenInUseAuthorization()
self.locationProvider.enable()
}
let locationProvider: LocationProvider
let locationManager: CLLocationManager
let routePlanner: TomTomSDKRoutePlannerOnline.OnlineRoutePlanner
var routePlanningOptions: TomTomSDKRoutingCommon.RoutePlanningOptions?
let displayedRouteSubject = PassthroughSubject<
TomTomSDKRoute.Route?,
Never
>()
}

Update your TomTomMapView by adding a NavigationController:

struct TomTomMapView {
var mapView = TomTomSDKMapDisplay.MapView()
var navigationController: NavigationController
}

Update the TomTomMapView UIViewRepresentable implementation:

extension TomTomMapView: UIViewRepresentable {
func makeUIView(context: Context) -> TomTomSDKMapDisplay.MapView {
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_: TomTomSDKMapDisplay.MapView, context: Context) {}
func makeCoordinator() -> MapCoordinator {
MapCoordinator(
mapView: mapView,
navigationController: navigationController
)
}
}

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

struct ContentView: View {
@ObservedObject
var navigationController = NavigationController()
var body: some View {
ZStack(alignment: .bottom) {
TomTomMapView(navigationController: navigationController)
}
}
}

Step 2: Planning a route

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

extension NavigationController {
enum RoutePlanError: Error {
case unableToPlanRoute
}
private func planRoute(
from origin: CLLocationCoordinate2D,
to destination: CLLocationCoordinate2D
) async throws
-> TomTomSDKNavigationEngines.RoutePlan {
routePlanningOptions = try createRoutePlanningOptions(
from: origin,
to: destination
)
let route = try await planRoute(
withRoutePlanner: routePlanner,
routePlanningOptions: routePlanningOptions!
)
return TomTomSDKNavigationEngines
.RoutePlan(route: route, routePlanningOptions: routePlanningOptions!)
}
private func createRoutePlanningOptions(
from origin: CLLocationCoordinate2D,
to destination: CLLocationCoordinate2D
)
throws -> TomTomSDKRoutingCommon.RoutePlanningOptions {
let itinerary = Itinerary(
origin: ItineraryPoint(coordinate: origin),
destination: ItineraryPoint(coordinate: destination)
)
let costModel = CostModel(routeType: .fast)
let guidanceOptions = try TomTomSDKRoutingCommon.GuidanceOptions()
let options = try TomTomSDKRoutingCommon.RoutePlanningOptions(
itinerary: itinerary,
costModel: costModel,
guidanceOptions: guidanceOptions
)
return options
}
private func planRoute(
withRoutePlanner routePlanner: OnlineRoutePlanner,
routePlanningOptions: TomTomSDKRoutingCommon.RoutePlanningOptions
) async throws
-> TomTomSDKRoute.Route {
return try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<TomTomSDKRoute.Route, Error>) in
routePlanner.planRoute(
options: routePlanningOptions,
onRouteReady: nil
) { result in
switch result {
case let .failure(error):
continuation.resume(throwing: error)
case let .success(response):
guard let route = response.routes?.first else {
continuation
.resume(throwing: RoutePlanError.unableToPlanRoute)
return
}
continuation.resume(returning: route)
}
}
}
}
}

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:

extension NavigationController {
func planAndDisplayRoute(destination: CLLocationCoordinate2D) {
Task { @MainActor in
do {
// Plan the route and add it to the map
guard let start = locationProvider.lastKnownLocation?.location.coordinate
else { return }
let routePlan = try await planRoute(
from: start,
to: destination
)
let route = routePlan.route
self.displayedRouteSubject.send(route)
} catch {
print("Error when planning a route: \(error)")
}
}
}
}

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:

extension MapCoordinator: TomTomSDKMapDisplay.MapDelegate {
func map(_ map: TomTomMap, onInteraction interaction: MapInteraction) {
switch interaction {
case let .longPressed(coordinate):
navigationController.planAndDisplayRoute(destination: coordinate)
default:
// Handle other gestures.
break
}
}
func map(_ map: TomTomMap, onCameraEvent event: CameraEvent) {
// Handle camera events.
}
}

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

extension MapCoordinator {
func observe(navigationController: NavigationController) {
navigationController.displayedRouteSubject.sink { [weak self] route in
guard let self = self else { return }
if let route = route {
self.addRouteToMap(route: route)
} else {
self.routeOnMap = nil
self.map?.removeRoutes()
}
}.store(in: &cancellableBag)
}
}

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

final class MapCoordinator {
init(
mapView: TomTomSDKMapDisplay.MapView,
navigationController: NavigationController
) {
self.mapView = mapView
self.navigationController = navigationController
observe(navigationController: navigationController)
}
private let mapView: TomTomSDKMapDisplay.MapView
private var map: TomTomSDKMapDisplay.TomTomMap?
private var cameraUpdated = false
private let navigationController: NavigationController
private var routeOnMap: TomTomSDKMapDisplay.Route?
private var cancellableBag = Set<AnyCancellable>()
}

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

extension MapCoordinator {
private func createMapRouteOptions(
coordinates: [CLLocationCoordinate2D]
)
-> TomTomSDKMapDisplay.RouteOptions {
var routeOptions = RouteOptions(coordinates: coordinates)
routeOptions.outlineWidth = 1
routeOptions.routeWidth = 5
routeOptions.color = .activeRoute
return routeOptions
}
func addRouteToMap(route: TomTomSDKRoute.Route) {
// Create the route options from the route geometry
// and adds it to the map.
let routeOptions = createMapRouteOptions(coordinates: route.geometry)
if let routeOnMap = try? map?.addRoute(routeOptions) {
self.routeOnMap = routeOnMap
// Zoom the map to make the route visible.
map?.zoomToRoutes(padding: 32)
}
}
func setCamera(trackingMode: TomTomSDKMapDisplay.CameraTrackingMode) {
map?.cameraTrackingMode = trackingMode
// Update the chevron position on the screen
// so it is not hidden behind the navigation panel.
if trackingMode == .followRouteDirection() || trackingMode == .followNorthUp() {
let cameraUpdate = CameraUpdate(positionMarkerVerticalOffset: 0.4)
map?.moveCamera(cameraUpdate)
}
}
}

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:

<plist version="1.0">
<dict>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
</dict>
</plist>

Step 2: Setting up navigation

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

final class NavigationController: ObservableObject {
convenience init() {
let textToSpeech = try! SystemTextToSpeechEngine()
let routePlanner = TomTomSDKRoutePlannerOnline
.OnlineRoutePlanner(apiKey: Keys.tomtomApiKey)
let locationProvider = DefaultCLLocationProvider()
let delay = Measurement.tt.seconds(1)
let simulatedLocationProvider = SimulatedLocationProvider(delay: delay)
let navigationConfiguration = OnlineTomTomNavigationFactory
.Configuration(
navigationTileStore: try! NavigationTileStore(config: NavigationTileStoreConfiguration(
apiKey: Keys
.tomtomApiKey
)),
locationProvider: simulatedLocationProvider,
routePlanner: routePlanner,
betterProposalAcceptanceMode: .automatic
)
let navigation = try! OnlineTomTomNavigationFactory
.create(configuration: navigationConfiguration)
let navigationModel = TomTomSDKNavigationUI.NavigationView
.ViewModel(navigation, tts: textToSpeech)
self.init(
locationProvider: locationProvider,
locationManager: CLLocationManager(),
simulatedLocationProvider: simulatedLocationProvider,
routePlanner: routePlanner,
navigation: navigation,
navigationModel: navigationModel
)
}
init(
locationProvider: LocationProvider,
locationManager: CLLocationManager,
simulatedLocationProvider: SimulatedLocationProvider,
routePlanner: TomTomSDKRoutePlannerOnline.OnlineRoutePlanner,
navigation: TomTomSDKNavigation.TomTomNavigation,
navigationModel: TomTomSDKNavigationUI.NavigationView.ViewModel
) {
self.locationProvider = locationProvider
self.locationManager = locationManager
self.simulatedLocationProvider = simulatedLocationProvider
self.routePlanner = routePlanner
self.navigation = navigation
self.navigationViewModel = navigationModel
self.navigation.addProgressObserver(self)
self.navigation.addRouteAddObserver(self)
self.navigation.addRouteRemoveObserver(self)
self.navigation.addRouteUpdateObserver(self)
self.navigation.addActiveRouteChangeObserver(self)
locationManager.requestWhenInUseAuthorization()
locationProvider.enable()
}
let locationProvider: LocationProvider
let locationManager: CLLocationManager
let simulatedLocationProvider: SimulatedLocationProvider
let routePlanner: TomTomSDKRoutePlannerOnline.OnlineRoutePlanner
let navigation: TomTomSDKNavigation.TomTomNavigation
let navigationViewModel: TomTomSDKNavigationUI.NavigationView.ViewModel
var routePlanningOptions: TomTomSDKRoutingCommon.RoutePlanningOptions?
var displayedRoutes: [UUID: TomTomSDKRoute.Route] = [:]
let displayedRouteSubject = PassthroughSubject<
TomTomSDKRoute.Route?,
Never
>()
let navigateRouteSubject = PassthroughSubject<
TomTomSDKRoute.Route?,
Never
>()
let progressOnRouteSubject = PassthroughSubject<
Measurement<UnitLength>,
Never
>()
let mapMatchedLocationProvider = PassthroughSubject<
LocationProvider,
Never
>()
@Published
var showNavigationView: Bool = false
}

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

extension NavigationController {
func navigateOn(route: TomTomSDKRoute.Route) {
Task { @MainActor in
guard let routePlanningOptions else { return }
self.displayedRouteSubject.send(nil) // clear previous route after replanning
self.displayedRouteSubject.send(route)
let routePlan = RoutePlan(route: route, routePlanningOptions: routePlanningOptions)
let navigationOptions = NavigationOptions(
activeRoutePlan: routePlan
)
self.navigationViewModel.start(navigationOptions)
// Use simulated location updates.
self.simulatedLocationProvider
.updateCoordinates(route.geometry, interpolate: true)
self.simulatedLocationProvider.enable()
self.mapMatchedLocationProvider
.send(navigation.mapMatchedLocationProvider)
self.navigateRouteSubject.send(route)
self.showNavigationView = true
}
}
func stopNavigating() {
displayedRouteSubject.send(nil)
navigationViewModel.stop()
simulatedLocationProvider.disable()
showNavigationView = false
}
}

Step 3: Starting navigation on the selected route

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

extension NavigationController: RouteDelegate {
func mapRoute(
_ route: TomTomSDKMapDisplay.Route,
didTapOnCoordinate coordinate: CLLocationCoordinate2D,
waypointIndex: Int
) {
guard let route = displayedRoutes[route.routeID]
else { return }
navigateOn(route: route)
}
}

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

func addRouteToMap(route: TomTomSDKRoute.Route, zoom: Bool = false) {
// Create the route options from the route geometry
// and add it to the map.
let routeOptions = createMapRouteOptions(coordinates: route.geometry)
if let routeOnMap = try? map?.addRoute(routeOptions) {
self.routeOnMap = routeOnMap
navigationController.displayedRoutes[routeOnMap.routeID] = route
routeOnMap.delegate = navigationController
// Zoom the map to make the route visible.
if !navigationController.showNavigationView {
map?.zoomToRoutes(padding: 32) // zoom to the route if no navigation in progress
}
}
}

Step 4: Observing navigation events

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

extension NavigationController: TomTomSDKNavigation.NavigationProgressObserver {
func didUpdateProgress(progress: RouteProgress) {
progressOnRouteSubject.send(progress.distanceAlongRoute)
}
}

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

extension NavigationController {
func onNavigationViewAction(
_ action: TomTomSDKNavigationUI.NavigationView.Action
) {
switch action {
case let .arrival(action):
onArrivalAction(action)
case let .instruction(action):
onInstructionAction(action)
case let .confirmation(action):
onConfirmationAction(action)
case let .error(action):
onErrorAction(action)
@unknown default:
/* YOUR CODE GOES HERE */
break
}
}
fileprivate func onArrivalAction(
_ action: TomTomSDKNavigationUI.NavigationView.ArrivalAction
) {
switch action {
case .close:
stopNavigating()
@unknown default:
/* YOUR CODE GOES HERE */
break
}
}
fileprivate func onInstructionAction(
_ action: TomTomSDKNavigationUI.NavigationView.InstructionAction
) {
switch action {
case let .tapSound(muted):
navigationViewModel.muteTextToSpeech(mute: !muted)
case .tapLanes:
navigationViewModel.hideLanes()
case .tapThen:
navigationViewModel.hideCombinedInstruction()
@unknown default:
/* YOUR CODE GOES HERE */
break
}
}
fileprivate func onConfirmationAction(
_ action: TomTomSDKNavigationUI.NavigationView.ConfirmationAction
) {
switch action {
case .yes:
stopNavigating()
case .no:
/* YOUR CODE GOES HERE */
break
@unknown default:
/* YOUR CODE GOES HERE */
break
}
}
fileprivate func onErrorAction(
_ action: TomTomSDKNavigationUI.NavigationView.ErrorAction
) {
/* YOUR CODE GOES HERE */
}
}

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
extension NavigationController: TomTomSDKNavigation.NavigationRouteAddObserver,
TomTomSDKNavigation.NavigationRouteRemoveObserver,
TomTomSDKNavigation.NavigationActiveRouteChangeObserver,
TomTomSDKNavigation.NavigationRouteUpdateObserver {
func didAddRoute(
route: TomTomSDKRoute.Route,
options: TomTomSDKRoutingCommon.RoutePlanningOptions,
reason: TomTomSDKNavigation.RouteAddedReason
) {
let triggeringReasons: [RouteAddedReason] = [.avoidBlockage, .deviated, .manuallyUpdated, .withinRange]
if triggeringReasons.contains(reason) {
displayedRouteSubject.send(nil) // clear previous route after replanning
displayedRouteSubject.send(route)
}
}
func didRemoveRoute(route: TomTomSDKRoute.Route, reason: TomTomSDKNavigation.RouteRemovedReason) {}
func didChangeActiveRoute(route: TomTomSDKRoute.Route) {}
func didUpdateRoute(route: TomTomSDKRoute.Route, reason: TomTomSDKNavigation.RouteUpdatedReason) {}
}

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

extension MapCoordinator {
func observe(navigationController: NavigationController) {
navigationController.displayedRouteSubject.sink { [weak self] route in
guard let self = self else { return }
if let route = route {
self.addRouteToMap(route: route)
} else {
self.navigationController.displayedRoutes = [:]
self.routeOnMap = nil
self.map?.removeRoutes()
self.setCamera(trackingMode: .none)
}
}.store(in: &cancellableBag)
navigationController.navigateRouteSubject
.sink { [weak self] route in
guard let self = self else { return }
if route != nil {
self.setCamera(trackingMode: .followRouteDirection())
} else {
self.setCamera(trackingMode: .followNorthUp())
}
}.store(in: &cancellableBag)
navigationController.progressOnRouteSubject
.sink { [weak self] progress in
self?.routeOnMap?.progressOnRoute = progress
}.store(in: &cancellableBag)
navigationController.mapMatchedLocationProvider
.sink { [weak self] locationProvider in
self?.map?.locationProvider = locationProvider
}.store(in: &cancellableBag)
}
}

Step 5: Displaying navigation UI

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

struct ContentView: View {
@ObservedObject
var navigationController = NavigationController()
var body: some View {
ZStack(alignment: .bottom) {
TomTomMapView(navigationController: navigationController)
if navigationController.showNavigationView {
NavigationView(
navigationController.navigationViewModel,
action: navigationController.onNavigationViewAction
)
}
}
}
}

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: