Custom location indicator
Provide a custom user location indicator layer during navigation.
/*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-examplesTo learn more about each example in this app, including descriptions and linksto documentation, see our docs: https://docs.mapbox.com/ios/navigation/examples/custom-location-indicator*/ import UIKitimport MapboxCoreNavigationimport MapboxNavigationimport MapboxDirectionsimport MapboxMaps class CustomUserLocationViewController: UIViewController, NavigationMapViewDelegate, NavigationViewControllerDelegate, UIGestureRecognizerDelegate { typealias ActionHandler = (UIAlertAction) -> Void var navigationMapView: NavigationMapView! {didSet {// After the start of active turn-by-turn navigation, the previous `navigationMapView` should be `nil` and removed from super view. It could avoid the location update in the background to disturb the turn-by-turn navigation guidance.if let navigationMapView = oldValue {navigationMapView.removeFromSuperview()} if navigationMapView != nil {setupNavigationMapView()}}} var routeResponse: RouteResponse? {didSet {guard let routes = routeResponse?.routes, let currentRoute = routes.first else {navigationMapView?.removeRoutes()navigationMapView?.removeRouteDurations()navigationMapView?.removeWaypoints()waypoints.removeAll()return}navigationMapView.show(routes)navigationMapView.showWaypoints(on: currentRoute)}} var waypoints: [Waypoint] = [] private let startButton = UIButton()private let clearButton = UIButton() override func viewDidLoad() {super.viewDidLoad() if navigationMapView == nil {navigationMapView = NavigationMapView(frame: view.bounds)}} func setupNavigationMapView() {navigationMapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]navigationMapView.delegate = selfnavigationMapView.userLocationStyle = .puck2D() let navigationViewportDataSource = NavigationViewportDataSource(navigationMapView.mapView, viewportDataSourceType: .raw)navigationViewportDataSource.options.followingCameraOptions.zoomUpdatesAllowed = falsenavigationViewportDataSource.followingMobileCamera.zoom = 15.0navigationMapView.navigationCamera.viewportDataSource = navigationViewportDataSource view.addSubview(navigationMapView) setupStartButton()setupClearButton()setupGestureRecognizers()} func setupClearButton() {clearButton.setTitle("Clear", for: .normal)clearButton.backgroundColor = .blueclearButton.layer.cornerRadius = 5clearButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) clearButton.addTarget(self, action: #selector(clearMap), for: .touchUpInside)view.addSubview(clearButton)clearButton.translatesAutoresizingMaskIntoConstraints = falseclearButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50).isActive = trueclearButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10).isActive = trueclearButton.titleLabel?.font = UIFont.systemFont(ofSize: 25)} func setupStartButton() {startButton.setTitle("Start", for: .normal)startButton.layer.cornerRadius = 5startButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)startButton.backgroundColor = .blue startButton.addTarget(self, action: #selector(performAction), for: .touchUpInside)view.addSubview(startButton)startButton.translatesAutoresizingMaskIntoConstraints = falsestartButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50).isActive = truestartButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10).isActive = truestartButton.titleLabel?.font = UIFont.systemFont(ofSize: 25)} func setupGestureRecognizers() {let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))navigationMapView.gestureRecognizers?.filter({ $0 is UILongPressGestureRecognizer }).forEach(longPressGestureRecognizer.require(toFail:))navigationMapView.addGestureRecognizer(longPressGestureRecognizer)} @objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {guard gesture.state == .began else { return }let gestureLocation = gesture.location(in: navigationMapView)let destinationCoordinate = navigationMapView.mapView.mapboxMap.coordinate(for: gestureLocation) if waypoints.count > 1 {waypoints = Array(waypoints.dropFirst())} let waypoint = Waypoint(coordinate: destinationCoordinate, name: "Dropped Pin #\(waypoints.endIndex + 1)")waypoint.targetCoordinate = destinationCoordinate// Change the coordinate accuracy of `Waypoint` to negative before adding it to the `waypoints`. Thus the route requested on the `waypoints` is considered viable.waypoint.coordinateAccuracy = -1waypoints.append(waypoint) requestRoute()} func requestRoute() {guard waypoints.count > 0 else { return }guard let currentCoordinate = navigationMapView.mapView.location.latestLocation?.coordinate else {print("User location is not valid. Make sure to enable Location Services.")return} let userWaypoint = Waypoint(coordinate: currentCoordinate)// Change the coordinate accuracy of `Waypoint` to negative before adding it to the `waypoints`. Thus the route requested on the `waypoints` is considered viable.userWaypoint.coordinateAccuracy = -1waypoints.insert(userWaypoint, at: 0)let navigationRouteOptions = NavigationRouteOptions(waypoints: waypoints) Directions.shared.calculate(navigationRouteOptions) { [weak self] (_, result) inswitch result {case .failure(let error):print(error.localizedDescription)self?.waypoints.removeLast()case .success(let response):guard let routes = response.routes else { return }self?.routeResponse = responseself?.navigationMapView.show(routes)if let currentRoute = routes.first {self?.navigationMapView.showWaypoints(on: currentRoute)}}}} @objc func clearMap(_ sender: Any) {routeResponse = nil} @objc func performAction(_ sender: Any) {guard routeResponse != nil else {let alertController = UIAlertController(title: "Create route",message: "Long tap on the map to create a route first.",preferredStyle: .alert)alertController.addAction(UIAlertAction(title: "OK", style: .default))return present(alertController, animated: true)}// Set Up the alert controller to switch between different userLocationStyle.let alertController = UIAlertController(title: "Choose UserLocationStyle",message: "Select the user location style",preferredStyle: .actionSheet) let courseView: ActionHandler = { _ in self.setupCourseView() }let defaultPuck2D: ActionHandler = { _ in self.setupDefaultPuck2D() }let invisiblePuck: ActionHandler = { _ in self.setupInvisiblePuck() }let puck2D: ActionHandler = { _ in self.setupCustomPuck2D() }let cancel: ActionHandler = { _ in } let actionPayloads: [(String, UIAlertAction.Style, ActionHandler?)] = [("Invisible Puck", .default, invisiblePuck),("Default Course View", .default, courseView),("2D Default Puck", .default, defaultPuck2D),("2D Arrow Puck", .default, puck2D),("Cancel", .cancel, cancel)] actionPayloads.map { payload in UIAlertAction(title: payload.0, style: payload.1, handler: payload.2) }.forEach(alertController.addAction(_:)) if let popoverController = alertController.popoverPresentationController {popoverController.sourceView = self.startButtonpopoverController.sourceRect = self.startButton.bounds} present(alertController, animated: true, completion: nil)} func setupCourseView() {// Given configuration to the `UserLocationStyle.courseView` through the customizing of `UserPuckCourseView`. Both `UserPuckCourseView` and `UserHaloView` are subclassable.// By default `NavigationMapView.userLocationStyle` property is set to `UserLocationStyle.courseView(_:)`.presentNavigationViewController(.courseView())} func setupDefaultPuck2D() {presentNavigationViewController(.puck2D())} func setupInvisiblePuck() {presentNavigationViewController(.none)} func setupCustomPuck2D() {// It's optional to set up `Puck2DConfiguration` to the `UserLocationStyle.puck2D`. Otherwise the default configuration for the `UserLocationStyle.puck2D` is `Puck2DConfiguration()`.var puck2DConfiguration = Puck2DConfiguration()if #available(iOS 13.0, *) {puck2DConfiguration.topImage = UIImage(systemName: "arrow.up")puck2DConfiguration.scale = .constant(2.0)} let userLocationStyle = UserLocationStyle.puck2D(configuration: puck2DConfiguration)presentNavigationViewController(userLocationStyle)} func presentNavigationViewController(_ userLocationStyle: UserLocationStyle? = nil) {guard let routeResponse = routeResponse else { return } 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.routeLineTracksTraversal = truenavigationViewController.delegate = selfnavigationViewController.modalPresentationStyle = .fullScreen // If not customizing the `NavigationMapView.userLocationStyle`, it defaults as the `UserLocationStyle.courseView(_:)`.navigationViewController.navigationMapView?.userLocationStyle = userLocationStyle navigationViewController.navigationMapView?.mapView.mapboxMap.style.uri = navigationMapView.mapView?.mapboxMap.style.uri present(navigationViewController, animated: true) {// When start navigation, the previous `navigationMapView` should be `nil` and removed from super view. The niled out `navigationMapView` could avoid the location provider sending location update in the background, which will disturb the turn-by-turn navigation guidance.self.navigationMapView = nil}} func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) {routeResponse = nildismiss(animated: true, completion: nil)if navigationMapView == nil {navigationMapView = NavigationMapView(frame: view.bounds)}}}