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)
}
}
}
}