Skip to main content

Custom navigation camera

Add custom data source and transitions to navigation camera.
CustomNavigationCameraViewController
/*
This code example is part of the Mapbox Navigation SDK for iOS demo app,
which you can build and run: https://github.com/mapbox/mapbox-navigation-ios-examples
To learn more about each example in this app, including descriptions and links
to documentation, see our docs: https://docs.mapbox.com/ios/navigation/examples/custom-navigation-camera
*/

import UIKit
import MapboxNavigation
import MapboxMaps
import MapboxDirections
import MapboxCoreNavigation

class CustomNavigationCameraViewController: UIViewController {

var navigationMapView: NavigationMapView!
var routeResponse: RouteResponse!
var startNavigationButton: UIButton!

// MARK: - UIViewController lifecycle methods

override func viewDidLoad() {
super.viewDidLoad()

setupNavigationMapView()
setupStartNavigationButton()
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

startNavigationButton.layer.cornerRadius = startNavigationButton.bounds.midY
startNavigationButton.clipsToBounds = true
startNavigationButton.setNeedsDisplay()
}

// MARK: - Setting-up methods

func setupNavigationMapView() {
navigationMapView = NavigationMapView(frame: view.bounds)
navigationMapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
navigationMapView.userLocationStyle = .puck2D()

// Modify default `NavigationViewportDataSource` and `NavigationCameraStateTransition` to change
// `NavigationCamera` behavior during free drive and when locations are provided by Maps SDK directly.
navigationMapView.navigationCamera.viewportDataSource = CustomViewportDataSource(navigationMapView.mapView)
navigationMapView.navigationCamera.cameraStateTransition = CustomCameraStateTransition(navigationMapView.mapView)

let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
navigationMapView.addGestureRecognizer(longPressGestureRecognizer)

view.addSubview(navigationMapView)
}

func setupStartNavigationButton() {
startNavigationButton = UIButton()
startNavigationButton.setTitle("Start Navigation", for: .normal)
startNavigationButton.translatesAutoresizingMaskIntoConstraints = false
startNavigationButton.backgroundColor = .lightGray
startNavigationButton.setTitleColor(.darkGray, for: .highlighted)
startNavigationButton.setTitleColor(.white, for: .normal)
startNavigationButton.contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
startNavigationButton.addTarget(self, action: #selector(startNavigationButtonPressed(_:)), for: .touchUpInside)
startNavigationButton.isHidden = true
view.addSubview(startNavigationButton)

startNavigationButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
startNavigationButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
}

@objc func startNavigationButtonPressed(_ sender: UIButton) {
let indexedRouteResponse = IndexedRouteResponse(routeResponse: routeResponse, routeIndex: 0)
let navigationService = MapboxNavigationService(indexedRouteResponse: indexedRouteResponse,
customRoutingProvider: NavigationSettings.shared.directions,
credentials: NavigationSettings.shared.directions.credentials,
simulating: simulationIsEnabled ? .always : .onPoorGPS)

let navigationOptions = NavigationOptions(navigationService: navigationService)
let navigationViewController = NavigationViewController(for: indexedRouteResponse,
navigationOptions: navigationOptions)
navigationViewController.modalPresentationStyle = .fullScreen

// Modify default `NavigationViewportDataSource` and `NavigationCameraStateTransition` to change
// `NavigationCamera` behavior during active guidance.
if let mapView = navigationViewController.navigationMapView?.mapView {
let customViewportDataSource = CustomViewportDataSource(mapView)
navigationViewController.navigationMapView?.navigationCamera.viewportDataSource = customViewportDataSource

let customCameraStateTransition = CustomCameraStateTransition(mapView)
navigationViewController.navigationMapView?.navigationCamera.cameraStateTransition = customCameraStateTransition
}

present(navigationViewController, animated: true, completion: nil)
}

@objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
guard gesture.state == .ended,
let origin = navigationMapView.mapView.location.latestLocation?.coordinate else { return }

let destination = navigationMapView.mapView.mapboxMap.coordinate(for: gesture.location(in: navigationMapView.mapView))
let navigationRouteOptions = NavigationRouteOptions(coordinates: [origin, destination])

Directions.shared.calculate(navigationRouteOptions) { [weak self] (_, result) in
switch result {
case .failure(let error):
NSLog("Error occured while requesting route: \(error.localizedDescription).")
case .success(let response):
guard let route = response.routes?.first else { return }

self?.startNavigationButton.isHidden = false
self?.routeResponse = response
self?.navigationMapView.show([route])
self?.navigationMapView.showWaypoints(on: route)
}
}
}
}
CustomCameraStateTransition
import MapboxMaps
import MapboxNavigation

/**
Custom implementation of Navigation Camera transitions, which conforms to `CameraStateTransition`
protocol.

To be able to use custom camera transitions user has to create instance of `CustomCameraStateTransition`
and then override with it default implementation, by modifying
`NavigationMapView.NavigationCamera.CameraStateTransition` or
`NavigationViewController.NavigationMapView.NavigationCamera.CameraStateTransition` properties.

By default Navigation SDK for iOS provides default implementation of `CameraStateTransition`
in `NavigationCameraStateTransition`.
*/
class CustomCameraStateTransition: CameraStateTransition {

weak var mapView: MapView?

required init(_ mapView: MapView) {
self.mapView = mapView
}

func transitionToFollowing(_ cameraOptions: CameraOptions, completion: @escaping (() -> Void)) {
mapView?.camera.ease(to: cameraOptions, duration: 0.5, curve: .linear, completion: { _ in
completion()
})
}

func transitionToOverview(_ cameraOptions: CameraOptions, completion: @escaping (() -> Void)) {
mapView?.camera.ease(to: cameraOptions, duration: 0.5, curve: .linear, completion: { _ in
completion()
})
}

func update(to cameraOptions: CameraOptions, state: NavigationCameraState) {
mapView?.camera.ease(to: cameraOptions, duration: 0.5, curve: .linear, completion: nil)
}

func cancelPendingTransition() {
mapView?.camera.cancelAnimations()
}
}
CustomViewportDataSource
import MapboxMaps
import MapboxNavigation
import MapboxCoreNavigation

/**
Custom implementation of Navigation Camera data source, which is used to fill and store
`CameraOptions` which will be later used by `CustomCameraStateTransition` for execution of
transitions and continuous camera updates.

To be able to use custom camera data source user has to create instance of `CustomCameraStateTransition`
and then override with it default implementation, by modifying
`NavigationMapView.NavigationCamera.ViewportDataSource` or
`NavigationViewController.NavigationMapView.NavigationCamera.ViewportDataSource` properties.

By default Navigation SDK for iOS provides default implementation of `ViewportDataSource`
in `NavigationViewportDataSource`.
*/
class CustomViewportDataSource: ViewportDataSource {

public weak var delegate: ViewportDataSourceDelegate?

public var followingMobileCamera: CameraOptions = CameraOptions()

public var followingCarPlayCamera: CameraOptions = CameraOptions()

public var overviewMobileCamera: CameraOptions = CameraOptions()

public var overviewCarPlayCamera: CameraOptions = CameraOptions()

weak var mapView: MapView?

// MARK: - Initializer methods

public required init(_ mapView: MapView) {
self.mapView = mapView
self.mapView?.location.addLocationConsumer(newConsumer: self)

subscribeForNotifications()
}

deinit {
unsubscribeFromNotifications()
}

// MARK: - Notifications observer methods

func subscribeForNotifications() {
// `CustomViewportDataSource` uses raw locations provided by `LocationConsumer` in
// free-drive mode and locations snapped to the road provided by
// `Notification.Name.routeControllerProgressDidChange` notification.
NotificationCenter.default.addObserver(self,
selector: #selector(progressDidChange(_:)),
name: .routeControllerProgressDidChange,
object: nil)
}

func unsubscribeFromNotifications() {
NotificationCenter.default.removeObserver(self,
name: .routeControllerProgressDidChange,
object: nil)
}

@objc func progressDidChange(_ notification: NSNotification) {
let location = notification.userInfo?[RouteController.NotificationUserInfoKey.locationKey] as? CLLocation
let routeProgress = notification.userInfo?[RouteController.NotificationUserInfoKey.routeProgressKey] as? RouteProgress
let cameraOptions = self.cameraOptions(location, routeProgress: routeProgress)

delegate?.viewportDataSource(self, didUpdate: cameraOptions)
}

func cameraOptions(_ location: CLLocation?, routeProgress: RouteProgress? = nil) -> [String: CameraOptions] {
followingMobileCamera.center = location?.coordinate
// Set the bearing of the `MapView` (measured in degrees clockwise from true north).
followingMobileCamera.bearing = location?.course
followingMobileCamera.padding = .zero
followingMobileCamera.zoom = 15.0
followingMobileCamera.pitch = 45.0

if let shape = routeProgress?.route.shape,
let camera = mapView?.mapboxMap.camera(for: .lineString(shape),
padding: UIEdgeInsets(top: 150.0, left: 10.0, bottom: 150.0, right: 10.0),
bearing: 0.0,
pitch: 0.0) {
overviewMobileCamera = camera
}

let cameraOptions = [
CameraOptions.followingMobileCamera: followingMobileCamera,
CameraOptions.overviewMobileCamera: overviewMobileCamera
]

return cameraOptions
}
}

// MARK: - LocationConsumer delegate

extension CustomViewportDataSource: LocationConsumer {

var shouldTrackLocation: Bool {
return true
}

func locationUpdate(newLocation: Location) {
let location = CLLocation(coordinate: newLocation.coordinate,
altitude: 0.0,
horizontalAccuracy: newLocation.horizontalAccuracy,
verticalAccuracy: 0.0,
course: newLocation.course,
speed: 0.0,
timestamp: Date())

let cameraOptions = self.cameraOptions(location)
delegate?.viewportDataSource(self, didUpdate: cameraOptions)
}
}
Was this example helpful?