Skill 12 · Mapbox Navigation Patterns
Subchapter 12.5
references/ios-navigation-specialized.mdMarkdown7 KBView on GitHub
Load this file for multi-stop routing, route-line styling, navigation camera, road cameras, and route alerts.
NavigationMapView APIs here are stack-independent — wrap NavigationMapView in . Defaults, catalog, and the sample-host vs API-stack rule live in .
UIViewRepresentableSnippets are NavSDK-focused (like Android’s reference): not full screens — omit permissions, full error UI, and app architecture.
Append intermediate Waypoints (user first), then NavigationRouteOptions(waypoints:). Catalog: Multiple Waypoints.
var waypoints: [Waypoint] = []
func requestRoute(to mapPoint: MapPoint, userLocation: CLLocation) async throws -> NavigationRoutes {
waypoints.append(Waypoint(coordinate: mapPoint.coordinate, name: mapPoint.name))
var requestWaypoints = waypoints
requestWaypoints.insert(Waypoint(location: userLocation), at: 0)
let options = NavigationRouteOptions(waypoints: requestWaypoints)
return try await mapboxNavigation.routingProvider()
.calculateRoutes(options: options)
.value
}
// Preview, then drop-in UI as usual:
// navigationMapView.showcase(navigationRoutes)
// present(NavigationViewController(navigationRoutes:navigationOptions:))On arrival during drop-in UI, implement NavigationViewControllerDelegate.navigationViewController(_:didArriveAt:).
NavigationMapView API — stack-independent (wrap NavigationMapView in a SwiftUI UIViewRepresentable). Customize preview and active-guidance route lines via NavigationMapViewDelegate / NavigationViewControllerDelegate layer factories. Catalog: Custom Route Lines Styling. Use identifier (main vs alternative_N, .casing) to pick colors.
func navigationMapView(
_ navigationMapView: NavigationMapView,
routeLineLayerWithIdentifier identifier: String,
sourceIdentifier: String
) -> LineLayer? {
var layer = LineLayer(id: identifier, source: sourceIdentifier)
let isMain = identifier.contains("main")
layer.lineColor = .constant(.init(isMain ? UIColor.systemGreen : UIColor.systemGray))
layer.lineWidth = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
RouteLineWidthByZoomLevel.multiplied(by: 1)
}
)
layer.lineJoin = .constant(.round)
layer.lineCap = .constant(.round)
return layer
}
func navigationMapView(
_ navigationMapView: NavigationMapView,
routeCasingLineLayerWithIdentifier identifier: String,
sourceIdentifier: String
) -> LineLayer? {
// Same pattern; typically a darker casing with a slightly larger width multiplier.
var layer = LineLayer(id: identifier, source: sourceIdentifier)
layer.lineColor = .constant(.init(UIColor.darkGray))
layer.lineWidth = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
RouteLineWidthByZoomLevel.multiplied(by: 1.2)
}
)
return layer
}
// Mirror the same two methods on NavigationViewControllerDelegate for active guidance.
// Optional: navigationView.navigationMapView.traversedRouteColor = .lightGrayNavigationMapView API — stack-independent (wrap NavigationMapView in a SwiftUI UIViewRepresentable). Default camera works via NavigationMapView + update(navigationCameraState:) (see CoreSDKExample). To customize framing/transitions, replace viewportDataSource and/or cameraStateTransition. Catalog: Custom Navigation Camera.
let navigationCamera = navigationMapView.navigationCamera
navigationCamera.viewportDataSource = CustomViewportDataSource(navigationMapView.mapView)
navigationCamera.cameraStateTransition = CustomCameraStateTransition(navigationMapView.mapView)
// Core / SwiftUI-driven state (CoreSDKExample):
// navigationMapView.update(navigationCameraState: .following) // or .idle, overview, etc.
// When handing the same map into drop-in UI:
let navigationOptions = NavigationOptions(
mapboxNavigation: mapboxNavigation,
voiceController: provider.routeVoiceController,
eventsManager: provider.eventsManager(),
navigationMapView: navigationMapView // reuse preview map + custom camera
)Implement ViewportDataSource / CameraStateTransition (see example NavigationCamera/ helpers) — do not only call follow/overview without feeding the data source.
NavigationMapView API — stack-independent (wrap NavigationMapView in a SwiftUI UIViewRepresentable). Take mapboxMap from the map view; give RoadCamerasManager Core dependencies via provider.navigatorHandle. Catalog: Road Cameras. Uses experimental SPI / MapboxNavigationCppRoadCameras.
import Combine
@_spi(ExperimentalMapboxAPI) import MapboxDirections
@_spi(MapboxInternal) import MapboxNavigationCore
@_spi(ExperimentalMapboxAPI) import MapboxNavigationCppRoadCameras
var options = NavigationRouteOptions(coordinates: [origin, destination])
options.attributeOptions.insert(.roadCamera)
func setupRoadCameras(on navigationMapView: NavigationMapView, provider: MapboxNavigationProvider) {
let mapboxMap = navigationMapView.mapView.mapboxMap
let manager = RoadCamerasManager(navigatorHandle: provider.navigatorHandle)
let mapController = RoadCamerasMapController(
map: mapboxMap,
manager: manager,
config: RoadCamerasConfig(
displayConfig: RoadCamerasDisplayConfig(startShowDistance: 1000),
iconProvider: nil // or custom RoadCamerasIconProvider
)
)
// Keep strong references to manager + mapController
manager.camerasAppearing.sink { /* upcoming cameras */ }.store(in: &subscriptions)
manager.camerasPassed.sink { _ in /* passed */ }.store(in: &subscriptions)
mapController.cameraClicked.sink { camera in /* camera.id */ }.store(in: &subscriptions)
}
// Drop-in NVC is the same NMV path:
// setupRoadCameras(on: navVC.navigationMapView!, provider: provider)Core RouteProgress.upcomingRouteAlerts is stack-independent. Optional custom top banner via NavigationOptions.topBanner is NVC chrome. Catalog: Route Alerts.
// Custom ContainerViewController as topBanner:
navigation.routeProgress
.sink { status in
guard let progress = status?.routeProgress else { return }
let alerts = progress.upcomingRouteAlerts.compactMap { alert -> String? in
let distance = Int64(alert.distanceToStart)
guard distance > 0, distance < 500 else { return nil }
// Use alert.roadObject.kind for a user-facing label
return "Alert in \(distance) m"
}
// Update banner primary label with alerts, or fall back to visual instruction
}
.store(in: &subscriptions)
let navigationOptions = NavigationOptions(
mapboxNavigation: mapboxNavigation,
voiceController: provider.routeVoiceController,
eventsManager: provider.eventsManager(),
topBanner: TopAlertsBarViewController(navigationProvider: provider)
)