← Files MapboxARCHIVED FILE
skills/mapbox-navigation-patterns/references/ios-navigation-sdk.md
17.7 KB · Sep 30, 2026 · 23:11 UTC
# iOS: Navigation SDK Patterns
App UI framework (SwiftUI vs UIKit) and navigation experience (drop-in vs fully custom Core) are **independent**.
**Default (matches official getting-started docs):** SwiftUI app shell + wrap drop-in `NavigationViewController` with `UIViewControllerRepresentable`. Do **not** build a fully custom Core UI unless the user explicitly wants that.
**Canonical getting-started:** [Add turn-by-turn navigation](https://docs.mapbox.com/ios/navigation/guides/get-started/) + [UIKitExample](https://github.com/mapbox/mapbox-navigation-ios/tree/main/Examples/UIKitExample) / AdditionalExamples → Basic.
**Fully custom Core UI (opt-in):** [CoreSDKExample](https://github.com/mapbox/mapbox-navigation-ios/tree/main/Examples/CoreSDKExample) — only when the user asks to customize the entire nav UI / avoid `NavigationViewController`.
## Sample host vs API stack
`AdditionalExamples` are UIKit _demo hosts_. A `UIViewController` sample does **not** mean the API is UIKit-only.
- **`NavigationMapView` APIs are stack-independent.** Wrap `NavigationMapView` in `UIViewRepresentable` and configure the same entities (route line, camera, waypoint / final-waypoint image, route callouts, road cameras).
- **True drop-in / UIKit chrome:** `NavigationViewController` top/bottom bars, styled NVC UI elements, embedding NVC. Those stay on the drop-in path.
```swift
struct NavigationMapViewWrapper: UIViewRepresentable {
func makeUIView(context: Context) -> NavigationMapView {
NavigationMapView(frame: .zero)
}
func updateUIView(_ view: NavigationMapView, context: Context) {
// Configure NMV APIs here: waypoint images, route-line delegate, camera, callouts, road cameras, …
view.delegate = context.coordinator
}
}
```
## Example patterns catalog
Self-contained like Maps/Search skills. Use the catalog to pick a pattern. **Do not** fetch upstream example source unless the user explicitly asks to open a specific sample file.
**NMV API** rows are stack-independent even when the sample is a UIKit host. **NVC chrome** rows are drop-in UIKit UI.
| Topic | Example | Stack / notes |
| -------------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| Drop-in nav in a SwiftUI app (default) | Docs getting-started + Basic | Wrap `NavigationViewController` in `UIViewControllerRepresentable` |
| Minimal drop-in navigation | `AdditionalExamples` → Basic | Drop-in NVC (sample is a UIKit host) |
| Full UIKit app shell | `UIKitExample` | UIKit app shell |
| Fully custom Core nav UI | `CoreSDKExample` | Core publishers — opt-in only |
| CarPlay | `CarPlayExample` | CarPlay |
| Advanced / alt routes + style | Advanced Implementation | NMV API (reuse map preview → active) |
| Multi-stop route | Multiple Waypoints | Core waypoints; **inline:** [ios-navigation-specialized.md](ios-navigation-specialized.md) |
| Custom route line styling | Custom Route Lines Styling | NMV API; **inline:** [ios-navigation-specialized.md](ios-navigation-specialized.md) |
| Custom navigation camera | Custom Navigation Camera | NMV API; **inline:** [ios-navigation-specialized.md](ios-navigation-specialized.md) |
| Road cameras on map | Road Cameras | NMV API (`mapView.mapboxMap` + Core `navigatorHandle`); **inline:** [ios-navigation-specialized.md](ios-navigation-specialized.md) |
| Route alerts | Route Alerts | Core `RouteProgress` (+ optional NVC `topBanner`); **inline:** [ios-navigation-specialized.md](ios-navigation-specialized.md) |
| Custom final waypoint image | Custom Final Waypoint | NMV API — wrap `NavigationMapView` in SwiftUI |
| Custom route callouts | Custom Route Callouts | NMV API — wrap `NavigationMapView` in SwiftUI |
| Embed `NavigationViewController` | Embedded View Controller | NVC chrome |
| Styled UI + map style | Styled UI Elements | NVC chrome |
| Directions beta query params | Directions API beta query parameters | Core — subclass `NavigationRouteOptions` |
| Custom waypoint styling | Custom Waypoint Styling | NMV API — wrap `NavigationMapView` in SwiftUI |
| Custom voice / audio | Custom Voice Controller | Core TTS |
| Custom top/bottom bars | Custom Top & Bottom Bars | NVC chrome |
| Offline TileStore / regions | Offline Regions | Core / TileStore |
| Record trip history | History Recording | Core |
| Replay trip history | History Replaying | Core (history files, not map-matched) |
| Electronic horizon / MPP | Electronic Horizon Events | Core |
| Custom road objects (e-horizon) | Custom Road Objects | Core |
| Declarative map styling | Declarative Map Styling | MapboxMap / Style DSL |
Upstream tree (optional deep dive only): [`Examples/`](https://github.com/mapbox/mapbox-navigation-ios/tree/main/Examples). Topic list source: `AdditionalExamples/Constants.swift` `listOfExamples`.
## Decision guide
| Need | Prefer |
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Add turn-by-turn to a SwiftUI app (default) | Wrap `NavigationViewController` in `UIViewControllerRepresentable` |
| UIKit app + drop-in nav | Present `NavigationViewController` directly |
| Fully custom nav chrome / no drop-in UI | CoreSDKExample-style Core + publishers (opt-in) |
| `NavigationMapView` customization (waypoints, route line, camera, callouts, road cameras) | Wrap `NavigationMapView` in SwiftUI `UIViewRepresentable` — stack-independent |
| Specialized topics (multi-stop, route line, camera, road cameras, alerts) | Load [`ios-navigation-specialized.md`](ios-navigation-specialized.md) |
| Other specialized topics | Match row in **Example patterns catalog** (catalog-only) |
---
## Before you code (iOS setup)
Greenfield apps need install prerequisites before the snippets below will run. See [Get started](https://docs.mapbox.com/ios/navigation/guides/install/) and **mapbox-token-security**.
Checklist:
1. **SPM** — `https://github.com/mapbox/mapbox-navigation-ios.git`; add both `MapboxNavigationCore` and `MapboxNavigationUIKit`
2. **Public token** — `MBXAccessToken` in `Info.plist` (`pk.*`). Stable Core and UIKit releases do not need a secret download token.
3. **Location** — `NSLocationWhenInUseUsageDescription` (and precise-location temporary usage dictionary when needed)
4. **Background modes** — `audio` and `location` in `UIBackgroundModes`
The snippets below are NavSDK-focused patterns (like Android’s reference): not full screens — omit permissions, full error UI, and app architecture.
---
## Default: SwiftUI + drop-in `NavigationViewController`
Keep a strong reference to `MapboxNavigationProvider`. Calculate routes with Core, then present the drop-in UI from SwiftUI via a representable.
```swift
import MapboxNavigationCore
import MapboxNavigationUIKit
import SwiftUI
struct NavigationViewControllerWrapper: UIViewControllerRepresentable {
let navigationRoutes: NavigationRoutes
let navigationOptions: NavigationOptions
func makeUIViewController(context: Context) -> NavigationViewController {
NavigationViewController(
navigationRoutes: navigationRoutes,
navigationOptions: navigationOptions
)
}
func updateUIViewController(_ uiViewController: NavigationViewController, context: Context) {}
}
@MainActor
final class NavigationSession: ObservableObject {
let provider = MapboxNavigationProvider(
coreConfig: CoreConfig(locationSource: .live, ttsConfig: .default)
)
@Published var navigationRoutes: NavigationRoutes?
func requestRoutes(from origin: CLLocationCoordinate2D, to destination: CLLocationCoordinate2D) async throws {
let options = NavigationRouteOptions(coordinates: [origin, destination])
navigationRoutes = try await provider.mapboxNavigation
.routingProvider()
.calculateRoutes(options: options)
.value
}
var navigationOptions: NavigationOptions {
NavigationOptions(
mapboxNavigation: provider.mapboxNavigation,
voiceController: provider.routeVoiceController,
eventsManager: provider.eventsManager(),
predictiveCacheManager: provider.predictiveCacheManager
)
}
}
// In a SwiftUI view, after routes are ready:
// NavigationViewControllerWrapper(
// navigationRoutes: routes,
// navigationOptions: session.navigationOptions
// )
// .ignoresSafeArea()
```
---
## UIKit app: present drop-in UI directly
```swift
import MapboxNavigationCore
import MapboxNavigationUIKit
import CoreLocation
class NavigationManager: UIViewController {
private let mapboxNavigationProvider: MapboxNavigationProvider
private var navigationViewController: NavigationViewController?
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
self.mapboxNavigationProvider = MapboxNavigationProvider(
coreConfig: CoreConfig(locationSource: .live, ttsConfig: .default)
)
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
required init?(coder: NSCoder) {
self.mapboxNavigationProvider = MapboxNavigationProvider(coreConfig: CoreConfig())
super.init(coder: coder)
}
func startNavigation() {
let origin = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
let destination = CLLocationCoordinate2D(latitude: 37.8044, longitude: -122.2711)
Task {
do {
let routeOptions = NavigationRouteOptions(coordinates: [origin, destination])
let navigationRoutes = try await mapboxNavigationProvider
.mapboxNavigation
.routingProvider()
.calculateRoutes(options: routeOptions)
.value
await showNavigationUI(with: navigationRoutes)
} catch {
print("Error calculating route: \(error.localizedDescription)")
}
}
}
@MainActor
func showNavigationUI(with navigationRoutes: NavigationRoutes) {
let navigationOptions = NavigationOptions(
mapboxNavigation: mapboxNavigationProvider.mapboxNavigation,
voiceController: mapboxNavigationProvider.routeVoiceController,
eventsManager: mapboxNavigationProvider.eventsManager(),
predictiveCacheManager: mapboxNavigationProvider.predictiveCacheManager
)
navigationViewController = NavigationViewController(
navigationRoutes: navigationRoutes,
navigationOptions: navigationOptions
)
navigationViewController?.modalPresentationStyle = .fullScreen
present(navigationViewController!, animated: true)
}
}
```
---
## Opt-in: fully custom Core UI (CoreSDKExample)
Use only when the user explicitly wants a custom navigation UI (no drop-in `NavigationViewController`). Drive chrome from Core publishers / `@Published` state.
```swift
import Combine
import MapboxNavigationCore
import SwiftUI
@MainActor
final class Navigation: ObservableObject {
@Published private(set) var visualInstruction: VisualInstructionBanner?
@Published private(set) var routeProgress: RouteProgress?
@Published private(set) var currentPreviewRoutes: NavigationRoutes?
// Keep a strong reference — do not create the provider only inside init and discard it.
private let provider: MapboxNavigationProvider
private let core: MapboxNavigation
private let voiceController: RouteVoiceController
init() {
let provider = MapboxNavigationProvider(
coreConfig: CoreConfig(locationSource: .live, ttsConfig: .default)
)
self.provider = provider
core = provider.mapboxNavigation
voiceController = provider.routeVoiceController
core.navigation().bannerInstructions
.map(\.visualInstruction)
.assign(to: &$visualInstruction)
core.navigation().routeProgress
.map { $0?.routeProgress }
.assign(to: &$routeProgress)
}
func requestRoutes(waypoints: [Waypoint]) async throws {
let options = NavigationRouteOptions(
waypoints: waypoints,
profileIdentifier: .automobileAvoidingTraffic
)
currentPreviewRoutes = try await core.routingProvider()
.calculateRoutes(options: options)
.value
}
func startActiveNavigation() {
guard let routes = currentPreviewRoutes else { return }
core.tripSession().startActiveGuidance(with: routes, startLegIndex: 0)
}
}
```
Session states: free drive → `startFreeDrive()`; active guidance → start active guidance on the trip session after preview routes; idle → `setToIdle()`.
---
## Voice guidance
```swift
MapboxNavigationProvider(coreConfig: CoreConfig(ttsConfig: .default))
CoreConfig(ttsConfig: .localOnly)
CoreConfig(ttsConfig: .custom(MyCustomSpeechSynthesizer()))
var options = NavigationRouteOptions(coordinates: [origin, destination])
options.locale = Locale(identifier: "es-ES")
options.distanceMeasurementSystem = .metric
```
Retain `provider.routeVoiceController` so TTS stays alive.
## Anti-pattern: recomputing progress manually
Use SDK fields on `RouteProgress` / leg / step progress — do not walk `legs`/`steps` on every update.
```swift
// ❌ Avoid — walks legs/steps and misses partial progress in the current step
let remaining = progress.route.legs
.flatMap(\.steps)
.dropFirst(progress.legIndex)
.reduce(0.0) { $0 + $1.distance }
// ✅ Prefer — total distance remaining on the route (use step/leg fields only when that scope is intentional)
let remaining = progress.distanceRemaining
// Distance to next maneuver: progress.currentLegProgress?.currentStepProgress.distanceRemaining
```
## Resources
- [Navigation SDK for iOS](https://docs.mapbox.com/ios/navigation/)
- [Get started](https://docs.mapbox.com/ios/navigation/guides/get-started/)
- [Examples](https://github.com/mapbox/mapbox-navigation-ios/tree/main/Examples)
- [UIKitExample](https://github.com/mapbox/mapbox-navigation-ios/tree/main/Examples/UIKitExample)
- [CoreSDKExample](https://github.com/mapbox/mapbox-navigation-ios/tree/main/Examples/CoreSDKExample) (custom UI opt-in)
SHA-256: 6ad8a2d0d665fd6538c4ed2edfc1fc236cde8f8582806e937bc8e3dc3579374e