All docsNavigation SDK for iOSGuidesFree-drive navigationTransition from free-drive to turn-by-turn navigation

Transition from free-drive to turn-by-turn navigation

If you use the NavigationMapView or NavigationView](https://docs.mapbox.com/ios/navigation/api/1234521054321/Turn-by-turn%20UI.html#/c:@M@MapboxNavigation@objc(cs)NavigationView) class for a free-drive navigation experience and NavigationViewController for turn-by-turn navigation, you can seamlessly transition between these activities.

Since NavigationMapView and NavigationView are usually presented as a child view of a certain view controller, you can use a custom transition based on the UIViewControllerTransitioningDelegate.

Requirements

To seamlessly transition between free-drive and turn-by-turn navigation sessions, your application needs to meet the following conditions:

Consider the following example, in which NavigationView is shown on the surface of the ViewController instance and PresentationTransition and DismissalTransition serve as custom animators:

class ViewController: UIViewController {
    
    var navigationView: NavigationView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        navigationView = NavigationView(frame: view.bounds)
        navigationView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(navigationView)
        
        NSLayoutConstraint.activate([
            navigationView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            navigationView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            navigationView.topAnchor.constraint(equalTo: view.topAnchor),
            navigationView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        let navigationViewportDataSource = NavigationViewportDataSource(navigationView.navigationMapView.mapView,
                                                                        viewportDataSourceType: .raw)
        navigationView.navigationMapView.navigationCamera.viewportDataSource = navigationViewportDataSource
        navigationView.navigationMapView.navigationCamera.follow()

        let navigationRouteOptions = NavigationRouteOptions(coordinates: [
            CLLocationCoordinate2D(latitude: 37.77766, longitude: -122.43199),
            CLLocationCoordinate2D(latitude: 37.77536, longitude: -122.43494)
        ])

        // Request a route and present `NavigationViewController`.
        Directions.shared.calculate(navigationRouteOptions) { [weak self] (_, result) in
            switch result {
            case .failure(let error):
                print("Error occured: \(error.localizedDescription)")
            case .success(let routeResponse):
                guard let self = self else { return }
                
                let navigationService = MapboxNavigationService(indexedRouteResponse: IndexedRouteResponse(routeResponse: routeResponse, routeIndex: 0),
                                                                credentials: NavigationSettings.shared.directions.credentials,
                                                                simulating: .always)
                
                let navigationOptions = NavigationOptions(navigationService: navigationService)
                
                let navigationViewController = NavigationViewController(for: IndexedRouteResponse(routeResponse: routeResponse, routeIndex: 0),
                                                                        navigationOptions: navigationOptions)
                navigationViewController.delegate = self

                // Make sure to set `transitioningDelegate` to be a current instance of `ViewController`.
                navigationViewController.transitioningDelegate = self
                
                // Make sure to present `NavigationViewController` in animated way.
                self.present(navigationViewController, animated: true, completion: nil)
            }
        }
    }
}

// Transition that is used for `NavigationViewController` presentation.
class PresentationTransition: NSObject, UIViewControllerAnimatedTransitioning {
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.0
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromViewController = transitionContext.viewController(forKey: .from) as? ViewController,
              let toViewController = transitionContext.viewController(forKey: .to) as? NavigationViewController else {
            transitionContext.completeTransition(false)
            return
        }
        
        // Re-use `NavigationMapView` instance in `NavigationViewController`.
        toViewController.navigationMapView = fromViewController.navigationView.navigationMapView
        
        transitionContext.containerView.addSubview(toViewController.view)
        transitionContext.completeTransition(true)
    }
}

// Transition that is used for `NavigationViewController` dismissal.
class DismissalTransition: NSObject, UIViewControllerAnimatedTransitioning {
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.0
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromViewController = transitionContext.viewController(forKey: .from) as? NavigationViewController,
              let navigationMapView = fromViewController.navigationMapView,
              let toViewController = transitionContext.viewController(forKey: .to) as? ViewController else {
            transitionContext.completeTransition(false)
            return
        }
        
        // Inject `NavigationMapView` instance that was previously used by `NavigationViewController` back to
        // `ViewController`.
        toViewController.navigationView.navigationMapView = navigationMapView
        
        transitionContext.containerView.addSubview(toViewController.view)
        transitionContext.completeTransition(true)
    }
}

extension ViewController: UIViewControllerTransitioningDelegate {

    public func animationController(forPresented presented: UIViewController,
                                    presenting: UIViewController,
                                    source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return PresentationTransition()
    }

    public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return DismissalTransition()
    }
}

Injecting NavigationMapView

When transitioning between free-drive and a turn-by-turn navigation, the NavigationViewController and another view controller can share an instance of NavigationMapView. Since the NavigationMapView configuration can be completely different in these two activities, make sure to perform the required changes to this configuration when completing an activity change.

The navigation camera viewport’s data source type

By default, whenever NavigationViewController is presented, it switches the navigation camera to observe location updates in RouteController.routeControllerProgressDidChange notifications, by setting NavigationViewportDataSource to the ViewportDataSourceType.active type. After dismissing NavigationViewController, you need to switch to the desired viewport data source type:

navigationViewController.dismiss(animated: true) { [weak self] in
    guard let self = self else { return }

    let navigationMapView = self.viewController.navigationMapView

    let navigationViewportDataSource = NavigationViewportDataSource(navigationMapView.mapView,
                                                                    viewportDataSourceType: .passive)

    navigationMapView.navigationCamera.viewportDataSource = navigationViewportDataSource
}

User location style

By default, NavigationViewController sets NavigationMapView.userLocationStyle to UserLocationStyle.courseView(_:) when the turn-by-turn navigation session starts. To preserve the user location style when transitioning from free-drive to turn-by-turn navigation:

viewController.present(navigationViewController,
                       animated: true,
                       completion: { [weak self] in
    guard let self = self else { return }

    navigationViewController.navigationMapView?.userLocationStyle = .puck2D()
}

Banners

For a smooth transition between free-drive and turn-by-turn navigation sessions, you can show and hide the top and bottom banners with animation. Use the navigationView property of NavigationViewController and PreviewViewController, get the container view using the NavigationView.topBannerContainerView or NavigationView.bottomBannerContainerView property, and call the container view’s show(animated:duration:animations:completion:) or hide(animated:duration:animations:completion:) method.