Skip to main content

Custom banners

Substitute the default top and bottom banners with custom view controllers.
CustomBarsViewController
/*
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-banner
*/

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] (_, result) in
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let response):
guard let strongSelf = self else {
return
}

// For demonstration purposes, simulate locations if the Simulate Navigation option is on.
let indexedRouteResponse = IndexedRouteResponse(routeResponse: response, routeIndex: 0)
let navigationService = MapboxNavigationService(indexedRouteResponse: indexedRouteResponse,
customRoutingProvider: NavigationSettings.shared.directions,
credentials: NavigationSettings.shared.directions.credentials,
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: indexedRouteResponse,
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)
navigationViewController.floatingButtons = []
navigationViewController.showsSpeedLimits = false
}
}
}
}

// MARK: - CustomTopBarViewController

class CustomTopBarViewController: ContainerViewController {
private lazy var instructionsBannerTopOffsetConstraint = {
return instructionsBannerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.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)
}()
private lazy var instructionsBannerWidthConstraint = {
return instructionsBannerView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9)
}()

// 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
instructionsBannerWidthConstraint.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: AnyObject {
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 func onCancel(_ sender: Any) {
delegate?.customBottomBannerDidCancel(self)
}
}
Was this example helpful?