Custom 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?