Skip to main content

Custom banners

A newer version of the Navigation SDK is available

This page uses v1.4.2 of the Mapbox Navigation SDK. A newer version of the SDK is available. Learn about the latest version, v2.17.0, in the Navigation SDK documentation.

ViewController
import Foundation
import UIKit
import MapboxCoreNavigation
import MapboxNavigation
import MapboxDirections

class CustomBarsViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

let origin = CLLocationCoordinate2DMake(37.77440680146262, -122.43539772352648)
let destination = CLLocationCoordinate2DMake(37.76556957793795, -122.42409811526268)
let routeOptions = NavigationRouteOptions(coordinates: [origin, destination])

Directions.shared.calculate(routeOptions) { [weak self] (session, result) in
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let response):
guard let route = response.routes?.first, let strongSelf = self else {
return
}

// For demonstration purposes, simulate locations if the Simulate Navigation option is on.
let navigationService = MapboxNavigationService(route: route, routeIndex: 0, routeOptions: routeOptions, simulating: simulationIsEnabled ? .always : .onPoorGPS)

// Pass your custom implementations of `topBanner` and/or `bottomBanner` to `NavigationOptions`
// If you do not specify them explicitly, `TopBannerViewController` and `BottomBannerViewController` will be used by default.
// Those are `Open`, so you can also check thier source for more examples of using standard UI controls!
let topBanner = CustomTopBarViewController()
let bottomBanner = CustomBottomBarViewController()
let navigationOptions = NavigationOptions(navigationService: navigationService, topBanner: topBanner, bottomBanner: bottomBanner)
let navigationViewController = NavigationViewController(for: route, routeIndex: 0, routeOptions: routeOptions, navigationOptions: navigationOptions)
bottomBanner.navigationViewController = navigationViewController

let parentSafeArea = navigationViewController.view.safeAreaLayoutGuide
let bannerHeight: CGFloat = 80.0
let verticalOffset: CGFloat = 20.0
let horizontalOffset: CGFloat = 10.0

// To change top and bottom banner size and position change layout constraints directly.
topBanner.view.topAnchor.constraint(equalTo: parentSafeArea.topAnchor).isActive = true

bottomBanner.view.heightAnchor.constraint(equalToConstant: bannerHeight).isActive = true
bottomBanner.view.bottomAnchor.constraint(equalTo: parentSafeArea.bottomAnchor, constant: -verticalOffset).isActive = true
bottomBanner.view.leadingAnchor.constraint(equalTo: parentSafeArea.leadingAnchor, constant: horizontalOffset).isActive = true
bottomBanner.view.trailingAnchor.constraint(equalTo: parentSafeArea.trailingAnchor, constant: -horizontalOffset).isActive = true

navigationViewController.modalPresentationStyle = .fullScreen

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

// MARK: - CustomTopBarViewController

class CustomTopBarViewController: ContainerViewController {
private lazy var instructionsBannerTopOffsetConstraint = {
return instructionsBannerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10)
}()
private lazy var centerOffset: CGFloat = calculateCenterOffset(with: view.bounds.size)
private lazy var instructionsBannerCenterOffsetConstraint = {
return instructionsBannerView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0)
}()

// You can Include one of the existing Views to display route-specific info
lazy var instructionsBannerView: InstructionsBannerView = {
let banner = InstructionsBannerView()
banner.translatesAutoresizingMaskIntoConstraints = false
banner.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
banner.layer.cornerRadius = 25
banner.layer.opacity = 0.75
banner.separatorView.isHidden = true
return banner
}()

override func viewDidLoad() {
view.addSubview(instructionsBannerView)

setupConstraints()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

updateConstraints()
}

private func setupConstraints() {
instructionsBannerCenterOffsetConstraint.isActive = true
instructionsBannerTopOffsetConstraint.isActive = true
}

private func updateConstraints() {
instructionsBannerCenterOffsetConstraint.constant = centerOffset
}

// MARK: - Device rotation

private func calculateCenterOffset(with size: CGSize) -> CGFloat {
return (size.height < size.width ? -size.width / 5 : 0)
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
centerOffset = calculateCenterOffset(with: size)
}

open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateConstraints()
}

// MARK: - NavigationServiceDelegate implementation

public func navigationService(_ service: NavigationService, didUpdate progress: RouteProgress, with location: CLLocation, rawLocation: CLLocation) {
// pass updated data to sub-views which also implement `NavigationServiceDelegate`
instructionsBannerView.updateDistance(for: progress.currentLegProgress.currentStepProgress)
}

public func navigationService(_ service: NavigationService, didPassVisualInstructionPoint instruction: VisualInstructionBanner, routeProgress: RouteProgress) {
instructionsBannerView.update(for: instruction)
}

public func navigationService(_ service: NavigationService, didRerouteAlong route: Route, at location: CLLocation?, proactive: Bool) {
instructionsBannerView.updateDistance(for: service.routeProgress.currentLegProgress.currentStepProgress)
}
}

// MARK: - CustomBottomBarViewController

class CustomBottomBarViewController: ContainerViewController, CustomBottomBannerViewDelegate {

weak var navigationViewController: NavigationViewController?

// Or you can implement your own UI elements
lazy var bannerView: CustomBottomBannerView = {
let banner = CustomBottomBannerView()
banner.translatesAutoresizingMaskIntoConstraints = false
banner.delegate = self
return banner
}()

override func loadView() {
super.loadView()

view.addSubview(bannerView)

let safeArea = view.layoutMarginsGuide
NSLayoutConstraint.activate([
bannerView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
bannerView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
bannerView.heightAnchor.constraint(equalTo: view.heightAnchor),
bannerView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor)
])
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setupConstraints()
}

private func setupConstraints() {
if let superview = view.superview?.superview {
view.bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
}

// MARK: - NavigationServiceDelegate implementation

func navigationService(_ service: NavigationService, didUpdate progress: RouteProgress, with location: CLLocation, rawLocation: CLLocation) {
// Update your controls manually
bannerView.progress = Float(progress.fractionTraveled)
bannerView.eta = "~\(Int(round(progress.durationRemaining / 60))) min"
}

// MARK: - CustomBottomBannerViewDelegate implementation

func customBottomBannerDidCancel(_ banner: CustomBottomBannerView) {
navigationViewController?.dismiss(animated: true,
completion: nil)
}
}
CustomBottomBannerView
import UIKit
import MapboxNavigation

protocol CustomBottomBannerViewDelegate: class {
func customBottomBannerDidCancel(_ banner: CustomBottomBannerView)
}

class CustomBottomBannerView: UIView {

@IBOutlet var contentView: UIView!
@IBOutlet weak var etaLabel: UILabel!
@IBOutlet weak var progressBar: UIProgressView!
@IBOutlet weak var cancelButton: UIButton!

var progress: Float {
get {
return progressBar.progress
}
set {
progressBar.setProgress(newValue, animated: false)
}
}

var eta: String? {
get {
return etaLabel.text
}
set {
etaLabel.text = newValue
}
}

weak var delegate: CustomBottomBannerViewDelegate?

private func initFromNib() {
Bundle.main.loadNibNamed(String(describing: CustomBottomBannerView.self),
owner: self,
options: nil)
addSubview(contentView)
contentView.frame = bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

progressBar.progressTintColor = .systemGreen
progressBar.layer.borderColor = UIColor.black.cgColor
progressBar.layer.borderWidth = 2
progressBar.layer.cornerRadius = 5

cancelButton.backgroundColor = .systemGray
cancelButton.layer.cornerRadius = 5
cancelButton.setTitleColor(.darkGray, for: .highlighted)

backgroundColor = UIColor.black.withAlphaComponent(0.3)
layer.cornerRadius = 10
}

override init(frame: CGRect) {
super.init(frame: frame)
initFromNib()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
initFromNib()
}

@IBAction @objc func onCancel(_ sender: Any) {
delegate?.customBottomBannerDidCancel(self)
}
}