Skip to main content

Quick navigation

Contents

The guide takes your through the basic steps needed to create a route and begin navigation.

Before getting started, make sure your project is set up correctly, permissions are granted, and authentication is completed.

Step 1: Set the layout

struct ContentView: View {
}

Include these imports in all the files:

import SwiftUI
import SearchUIPlugin
import CoreLocation
import TrimbleMapsNavigation
import TrimbleMapsCoreNavigation
import TrimbleMapsWebservicesClient
import Combine
import TrimbleMapsAccounts

Step 2: Initialize the NavigationView and Stops

The NavigationView effectively brings the navigation experience and UI to the screen, attaching to the view we made above. Inside the ContentView struct (outside the View), we’ll create a geocode function to initialize the source and destination.

private func geocode() {
    viewModel.tripPreviewViewModel.trip.origin = CLLocation(latitude: 40.7128, longitude: -74.0060)
    let destination = CLLocationCoordinate2D(latitude: 40.349542, longitude: -74.660237)
}

In the ContentView, we’ll initialize a viewModel, where we’ll have our navigation view controller, and tripPreviewViewModel.

struct TrimbleMapsMapView: UIViewRepresentable {
    @EnvironmentObject var viewModel: MapViewModel

    class MapViewModel: ObservableObject {
        @Published var navigationViewController: NavigationViewController?
        @Published var tripPreviewViewModel: TripPreviewViewModel = {
            var placeOptions = PlaceOptions()
            placeOptions.maxResults = 10
            let tripPreviewOptions = TripPreviewOptions(placeOptions: placeOptions)
            return TripPreviewViewModel.create(tripPreviewOptions: tripPreviewOptions)
        }()
    }
}

Now you can add a stop for your trip in the viewModel and create a route.

private func geocode() {
        viewModel.tripPreviewViewModel.trip.origin = CLLocation(latitude: 40.7128, longitude: -74.0060)
        let destination = CLLocationCoordinate2D(latitude: 40.349542, longitude: -74.660237)
        let geocoder = TMGeocoderClient.shared
        let region = AccountManager.default.region.queryValue
        let options = TMGeocoderParams(region: region, query: "\(destination.latitude), \(destination.longitude)")
        geocoder.geocode(options) { result, error in
            // sort results by distance
            if error == nil, let result = result?.locations.first {
                self.viewModel.tripPreviewViewModel.addStop(tripStop: TripStop(location: result))
                self.recalculateRoute()
            }
        }
    }

    private func recalculateRoute() {
        let callback: TMDirections.RouteCompletionHandler = { [self] session, result in
            switch result {
            case .success(let routeResponse):
                self.viewModel.updateTrip(trip: viewModel.tripPreviewViewModel.trip, routes: routeResponse.routes!)
            case .failure(let directionsError):
                NSLog("Error recalculating route: \(directionsError)")
            }
        }
        if viewModel.tripPreviewViewModel.trip.status == .Planned {
            viewModel.tripPreviewViewModel.trip.fetchFullRoute(callback: callback)
        } else {
            viewModel.tripPreviewViewModel.trip.fetchRemainingRoute(callback: callback)
        }
    }

Step 3: Update the trip in the MapViewModel

func updateTrip(trip: TrimbleMapsTrip, routes: [Route]) {
            let routeIndex = 0
            if let navigationViewController = navigationViewController {
                navigationViewController.update(newTrip: trip, newRoutes: routes)
                return
            }

            let routeOptions = try! trip.fullTripRouteOptions()

            let styles: [Style] = [TrimbleDayStyle()]
            let routeRefreshOptions = RouteRefreshOptions(isRouteRefreshEnabled: true,
                                                          refreshInterval: 60,
                                                          requestFasterRoute: true,
                                                          automaticAcceptFasterRoute: true)

            var simulate: SimulationMode = SimulationMode(rawValue: SimulationMode.always.rawValue)!

            let simulationMode: SimulationMode = simulate

            let navigationService = TrimbleMapsNavigationService(route: routes[routeIndex],
                                                            routeIndex: routeIndex,
                                                            routeOptions: routeOptions,
                                                            routeRefreshOptions: routeRefreshOptions,
                                                            simulating: simulationMode)

            //Implement beep before instruction setting
            var speechSynthesizer: SpeechSynthesizing = SystemSpeechSynthesizer()
            speechSynthesizer = BeepingSpeechSynthesizer(speechSynthesizer)


            let routeVoiceController = RouteVoiceController(navigationService: navigationService, speechSynthesizer: speechSynthesizer)

            let navigationOptions = NavigationOptions(styles: styles,
                                                      navigationService: navigationService,
                                                      voiceController: routeVoiceController,
                                                      showTripPreviewButton: true)

            let navigationViewController = NavigationViewController(trip: trip,
                                                                    for: routes[routeIndex], routeIndex:
                                                                        routeIndex,
                                                                    routeOptions: routeOptions,
                                                                    navigationOptions: navigationOptions)

            navigationViewController.routeLineTracksTraversal = true

            if (currentColorScheme == TMGLStyle.mobileDayStyleURL) {
                navigationViewController.styleManager.applyStyle(type: StyleType.day)
            } else {
                navigationViewController.styleManager.applyStyle(type: StyleType.night)
            }
            navigationViewController.showsSpeedLimits = showSpeedLimit
            navigationViewController.showsSpeedAlerts = showSpeedAlert

            self.navigationViewController = navigationViewController
        }

Step 4: Create a Coordinator to set our camera and style for the map.

class Coordinator: NSObject, TMGLMapViewDelegate {
        let viewModel: MapViewModel
        init(viewModel: MapViewModel) {
            self.viewModel = viewModel
        }
        func mapView(_ mapView: TMGLMapView, viewFor annotation: TMGLAnnotation) -> TMGLAnnotationView? {
            return nil
        }
        func mapView(_ mapView: TMGLMapView, annotationCanShowCallout annotation: TMGLAnnotation) -> Bool {
            return true
        }
        func mapView(_ mapView: TMGLMapView, didFinishLoading style: TMGLStyle) {
            viewModel.currentStyle = style
        }
        func mapView(_ mapView: TMGLMapView, didUpdate userLocation: TMGLUserLocation?) {
            guard let location = userLocation?.location else { return }
            // update the trip
            viewModel.tripPreviewViewModel.trip.origin = location
            // after that just update the origin silently
            let centerOnOrigin = viewModel.routeOrigin == nil
            viewModel.routeOrigin = location.coordinate
            if centerOnOrigin {
                viewModel.mapView.setCenter(location.coordinate, zoomLevel: 9, animated: false)
            }
        }
    }

After updating the navigation details using update, we need to display the NavigationView.

struct TrimbleMapsNavigationView: UIViewControllerRepresentable {

    var viewModel: TrimbleMapsMapView.MapViewModel
    let navDelegate = NavDelegate()
    var navigationViewController: NavigationViewController

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    class Coordinator: NSObject {
        var parent: TrimbleMapsNavigationView

        init(parent: TrimbleMapsNavigationView) {
            self.parent = parent
        }

        @objc func handleSearch(_ sender: Any) {
            parent.handleSearch()
        }
    }
    func makeUIViewController(context: Context) -> UIViewController {
        navDelegate.viewModel = viewModel
        navigationViewController.delegate = navDelegate
        navigationViewController.automaticallyAdjustsStyleForTimeOfDay = false
        navigationViewController.floatingButtons?.first?.addTarget(self, action: #selector(Coordinator.handleSearch(_:)), for: .touchUpInside)

        return navigationViewController
    }

    func handleSearch() {
        viewModel.isDynamicSearchMode = true
    }
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        navDelegate.viewModel = viewModel
        navigationViewController.delegate = navDelegate
        navigationViewController.floatingButtons?.first?.addAction(UIAction(handler: { action in
            viewModel.isDynamicSearchMode = true
        }), for: .touchUpInside)
    }

    class NavDelegate: NavigationViewControllerDelegate {

        var viewModel: TrimbleMapsMapView.MapViewModel! = nil

        func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) {
            // Perform last trip update before ending navigation
            if canceled {
                self.viewModel.tripPreviewViewModel.trip.status = .Canceled
            }
            endNavigation(navigationViewController: navigationViewController)
        }

        func endNavigation(navigationViewController: NavigationViewController) {
            navigationViewController.navigationService.stop()
            viewModel.isNavigationEnded = true
            navigationViewController.dismiss(animated: true) {
                // Capture the last location before resetting
                var origin = self.viewModel.tripPreviewViewModel.trip.lastRawLocation
                if origin == nil, let routeOrigin = self.viewModel.routeOrigin {
                    origin = CLLocation(latitude: routeOrigin.latitude, longitude: routeOrigin.longitude)
                }
                // Reset the view model and update origin
                self.viewModel.reset()
                if let originCoordinate = origin?.coordinate {
                    self.viewModel.routeOrigin = originCoordinate
                    self.viewModel.tripPreviewViewModel.trip.origin = origin
                }
                // Ensure navigationViewController is cleared
                self.viewModel.navigationViewController = nil
            }
        }

        func navigationViewControllerDidOpenTripPreview(_ navigationViewController: NavigationViewController) {
            viewModel.isDynamicTripPreview = true
        }
    }
}

Update your ContentView to call the updated trip.

import SwiftUI
import SearchUIPlugin
import CoreLocation
import TrimbleMapsNavigation
import TrimbleMapsCoreNavigation
import TrimbleMapsWebservicesClient
import Combine
import TrimbleMapsAccounts

struct ContentView: View {
    @StateObject var viewModel = TrimbleMapsMapView.MapViewModel()
    public var routeRequestCallback: TMDirections.RouteCompletionHandler?
    var body: some View {
        if let navigationViewController = viewModel.navigationViewController {
            TrimbleMapsNavigationView(viewModel: viewModel, navigationViewController: navigationViewController)
        }
        if viewModel.isNavigationEnded == true {
            Text("Home")
        } else {
            ZStack(alignment: .top) {
                Text("")
                    .onAppear {
                        geocode()
                    }
                    .background(Color.clear)
            }
        }
    }
    private func geocode() {
        viewModel.tripPreviewViewModel.trip.origin = CLLocation(latitude: 40.7128, longitude: -74.0060)
        let destination = CLLocationCoordinate2D(latitude: 40.349542, longitude: -74.660237)
        let geocoder = TMGeocoderClient.shared
        let region = AccountManager.default.region.queryValue
        let options = TMGeocoderParams(region: region, query: "\(destination.latitude), \(destination.longitude)")
        geocoder.geocode(options) { result, error in
            // sort results by distance
            if error == nil, let result = result?.locations.first {
                self.viewModel.tripPreviewViewModel.addStop(tripStop: TripStop(location: result))
                self.recalculateRoute()
            }
        }
    }

    private func recalculateRoute() {
        let callback: TMDirections.RouteCompletionHandler = { [self] session, result in
            switch result {
            case .success(let routeResponse):
                self.viewModel.updateTrip(trip: viewModel.tripPreviewViewModel.trip, routes: routeResponse.routes!)
            case .failure(let directionsError):
                NSLog("Error recalculating route: \(directionsError)")
            }
        }
        if viewModel.tripPreviewViewModel.trip.status == .Planned {
            viewModel.tripPreviewViewModel.trip.fetchFullRoute(callback: callback)
        } else {
            viewModel.tripPreviewViewModel.trip.fetchRemainingRoute(callback: callback)
        }
    }
}


struct TrimbleMapsMapView: UIViewRepresentable {
    @EnvironmentObject var viewModel: MapViewModel
    let tripStops: [TripStop] = []
    func makeCoordinator() -> Coordinator {
        return Coordinator(viewModel: viewModel)
    }
    func makeUIView(context: Context) -> TMGLMapView {
        viewModel.mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        viewModel.mapView.delegate = context.coordinator
        viewModel.mapView.showsUserLocation = true
        return viewModel.mapView
    }
    func updateUIView(_ uiView: TMGLMapView, context: Context) {}
    class MapViewModel: ObservableObject {
        @Published var tripPreviewViewModel: TripPreviewViewModel = {
            var placeOptions = PlaceOptions()
            placeOptions.maxResults = 10
            let tripPreviewOptions = TripPreviewOptions(placeOptions: placeOptions)
            return TripPreviewViewModel.create(tripPreviewOptions: tripPreviewOptions)
        }()
        @Published var isDynamicTripPreview = false
        @Published var isDynamicSearchMode = false
        @Published var routeOrigin: CLLocationCoordinate2D?
        @Published var routeDestination: CLLocationCoordinate2D?
        @Published var navigationViewController: NavigationViewController?
        @Published var isNavigationEnded: Bool = false
        let mapView: TMGLMapView = TMGLMapView(frame: .zero)
        var currentStyle: TMGLStyle?
        private var currentColorScheme: URL?
        @Published var showSpeedLimit: Bool = false
        @Published var showSpeedAlert: Bool = false
        func setShowSpeedLimit(_ show: Bool) {
            showSpeedLimit = show
            if let navigationViewController = navigationViewController {
                navigationViewController.showsSpeedLimits = showSpeedLimit
            }
        }
        func setShowSpeedAlert(_ show: Bool) {
            showSpeedAlert = show
            if let navigationViewController = navigationViewController {
                navigationViewController.showsSpeedAlerts = showSpeedAlert
            }
        }
        func setStyleUrl(_ styleUrl: URL) {
            currentColorScheme = styleUrl
            mapView.styleURL = styleUrl
            tripPreviewViewModel.tripPreviewMapModel.navigationMapView.styleURL = styleUrl
            if styleUrl == TMGLStyle.mobileDayStyleURL {
                navigationViewController?.styleManager.applyStyle(type: StyleType.day)
            } else {
                navigationViewController?.styleManager.applyStyle(type: StyleType.night)
            }
        }
        func reset() {
            tripPreviewViewModel = TripPreviewViewModel.create(tripPreviewOptions: tripPreviewViewModel.tripPreviewOptions)
            tripPreviewViewModel.tripPreviewMapModel.navigationMapView.styleURL = currentColorScheme
            routeOrigin = nil
            routeDestination = nil
            navigationViewController = nil
            currentStyle = nil
        }
        func updateTrip(trip: TrimbleMapsTrip, routes: [Route]) {
            let routeIndex = 0
            if let navigationViewController = navigationViewController {
                navigationViewController.update(newTrip: trip, newRoutes: routes)
                return
            }
            let routeOptions = try! trip.fullTripRouteOptions()
            let styles: [Style] = [TrimbleDayStyle()]
            let routeRefreshOptions = RouteRefreshOptions(isRouteRefreshEnabled: true,
                                                          refreshInterval: 60,
                                                          requestFasterRoute: true,
                                                          automaticAcceptFasterRoute: true)
            var simulate: SimulationMode = SimulationMode(rawValue: SimulationMode.always.rawValue)!
            let simulationMode: SimulationMode = simulate
            let navigationService = TrimbleMapsNavigationService(route: routes[routeIndex],
                                                                 routeIndex: routeIndex,
                                                                 routeOptions: routeOptions,
                                                                 routeRefreshOptions: routeRefreshOptions,
                                                                 simulating: simulationMode)
            var speechSynthesizer: SpeechSynthesizing = SystemSpeechSynthesizer()
            speechSynthesizer = BeepingSpeechSynthesizer(speechSynthesizer)
            let routeVoiceController = RouteVoiceController(navigationService: navigationService, speechSynthesizer: speechSynthesizer)
            let navigationOptions = NavigationOptions(styles: styles,
                                                      navigationService: navigationService,
                                                      voiceController: routeVoiceController,
                                                      showTripPreviewButton: true)
            let navigationViewController = NavigationViewController(trip: trip,
                                                                    for: routes[routeIndex],
                                                                    routeIndex: routeIndex,
                                                                    routeOptions: routeOptions,
                                                                    navigationOptions: navigationOptions)
            navigationViewController.routeLineTracksTraversal = true
            if currentColorScheme == TMGLStyle.mobileDayStyleURL {
                navigationViewController.styleManager.applyStyle(type: StyleType.day)
            } else {
                navigationViewController.styleManager.applyStyle(type: StyleType.night)
            }
            navigationViewController.showsSpeedLimits = showSpeedLimit
            navigationViewController.showsSpeedAlerts = showSpeedAlert
            self.navigationViewController = navigationViewController
        }
    }


    class Coordinator: NSObject, TMGLMapViewDelegate {
        let viewModel: MapViewModel
        init(viewModel: MapViewModel) {
            self.viewModel = viewModel
        }
        func mapView(_ mapView: TMGLMapView, viewFor annotation: TMGLAnnotation) -> TMGLAnnotationView? {
            return nil
        }
        func mapView(_ mapView: TMGLMapView, annotationCanShowCallout annotation: TMGLAnnotation) -> Bool {
            return true
        }
        func mapView(_ mapView: TMGLMapView, didFinishLoading style: TMGLStyle) {
            viewModel.currentStyle = style
        }
        func mapView(_ mapView: TMGLMapView, didUpdate userLocation: TMGLUserLocation?) {
            guard let location = userLocation?.location else { return }
            // update the trip
            viewModel.tripPreviewViewModel.trip.origin = location
            // after that just update the origin silently
            let centerOnOrigin = viewModel.routeOrigin == nil
            viewModel.routeOrigin = location.coordinate
            if centerOnOrigin {
                viewModel.mapView.setCenter(location.coordinate, zoomLevel: 9, animated: false)
            }
        }
    }
}
Last updated March 28, 2025.
Contents