Beta
Navigation SDK for iOS v2

Custom navigation camera

CustomNavigationCameraViewController
import UIKit
import MapboxNavigation
import MapboxMaps
import MapboxDirections
import MapboxCoreNavigation
class CustomNavigationCameraViewController: UIViewController {
var navigationMapView: NavigationMapView!
var route: Route!
var navigationRouteOptions: NavigationRouteOptions!
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 navigationService = MapboxNavigationService(route: route,
routeIndex: 0,
routeOptions: navigationRouteOptions,
simulating: simulationIsEnabled ? .always : .onPoorGPS)
let navigationOptions = NavigationOptions(navigationService: navigationService)
let navigationViewController = NavigationViewController(for: route,
routeIndex: 0,
routeOptions: navigationRouteOptions,
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))
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?.route = route
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)
}
}