Display the user's location
This code demonstrates how to add a location button to a map using the Mapbox Maps SDK for iOS. The ViewController
initializes a MapView
with camera settings and style options and adds a button to toggle user location tracking. The location button dynamically updates the map's camera to center on the user's location when tracking is enabled by listening for the onLocationChange
observer.
Additionally, the example includes a button to toggle the visibility of a "bearing image" on the location puck and a UISegmentedControl
for switching between map styles. Gesture interactions are handled to disable tracking when the user interacts with the map manually, ensuring a seamless and intuitive experience.
This example code is part of the Maps SDK for iOS Examples App, a working iOS project available on Github. iOS developers are encouraged to run the examples app locally to interact with this example in an emulator and explore other features of the Maps SDK.
See our Run the Maps SDK for iOS Examples App tutorial for step-by-step instructions.
import UIKit
import MapboxMaps
final class ViewController: UIViewController {
private var cancelables = Set<AnyCancelable>()
private var locationTrackingCancellation: AnyCancelable?
private var mapView: MapView!
private lazy var toggleBearingImageButton = UIButton(frame: .zero)
private lazy var trackingButton = UIButton(frame: .zero)
private lazy var styleToggle = UISegmentedControl(items: Style.allCases.map(\.name))
private var style: Style = .standard {
didSet {
mapView.mapboxMap.styleURI = style.uri
}
}
private var showsBearingImage: Bool = false {
didSet {
syncPuckAndButton()
}
}
override public func viewDidLoad() {
super.viewDidLoad()
// Set initial camera settings
let cameraOptions = CameraOptions(center: CLLocationCoordinate2D(latitude: 37.26301831966747, longitude: -121.97647612483807), zoom: 10)
let options = MapInitOptions(cameraOptions: cameraOptions, styleURI: style.uri)
mapView = MapView(frame: view.bounds, mapInitOptions: options)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)
addStyleToggle()
// Setup and create button for toggling show bearing image
setupToggleShowBearingImageButton()
setupLocationButton()
// Add user position icon to the map with location indicator layer
mapView.location.options.puckType = .puck2D()
mapView.location.options.puckBearingEnabled = true
mapView.gestures.delegate = self
// Update the camera's centerCoordinate when a locationUpdate is received.
startTracking()
}
public
@objc func showHideBearingImage() {
showsBearingImage.toggle()
}
func syncPuckAndButton() {
// Update puck config
let configuration = Puck2DConfiguration.makeDefault(showBearing: showsBearingImage)
mapView.location.options.puckType = .puck2D(configuration)
// Update button title
let title: String = showsBearingImage ? "Hide bearing image" : "Show bearing image"
toggleBearingImageButton.setTitle(title, for: .normal)
}
private func setupToggleShowBearingImageButton() {
// Styling
toggleBearingImageButton.backgroundColor = .systemBlue
toggleBearingImageButton.addTarget(self, action: #selector(showHideBearingImage), for: .touchUpInside)
toggleBearingImageButton.setTitleColor(.white, for: .normal)
toggleBearingImageButton.layer.cornerRadius = 4
toggleBearingImageButton.clipsToBounds = true
toggleBearingImageButton.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
toggleBearingImageButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(toggleBearingImageButton)
// Constraints
toggleBearingImageButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
toggleBearingImageButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true
toggleBearingImageButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -70.0).isActive = true
syncPuckAndButton()
}
private func setupLocationButton() {
trackingButton.addTarget(self, action: #selector(switchTracking), for: .touchUpInside)
if #available(iOS 13.0, *) {
trackingButton.setImage(UIImage(systemName: "location.fill"), for: .normal)
} else {
trackingButton.setTitle("No tracking", for: .normal)
}
let buttonWidth = 44.0
trackingButton.translatesAutoresizingMaskIntoConstraints = false
trackingButton.backgroundColor = UIColor(white: 0.97, alpha: 1)
trackingButton.layer.cornerRadius = buttonWidth/2
trackingButton.layer.shadowOffset = CGSize(width: -1, height: 1)
trackingButton.layer.shadowColor = UIColor.black.cgColor
trackingButton.layer.shadowOpacity = 0.5
view.addSubview(trackingButton)
NSLayoutConstraint.activate([
trackingButton.trailingAnchor.constraint(equalTo: toggleBearingImageButton.trailingAnchor),
trackingButton.bottomAnchor.constraint(equalTo: toggleBearingImageButton.topAnchor, constant: -20),
trackingButton.widthAnchor.constraint(equalTo: trackingButton.heightAnchor),
trackingButton.widthAnchor.constraint(equalToConstant: buttonWidth)
])
}
@objc func switchStyle(sender: UISegmentedControl) {
style = Style(rawValue: sender.selectedSegmentIndex) ?? . satelliteStreets
}
@objc func switchTracking() {
let isTrackingNow = locationTrackingCancellation != nil
if isTrackingNow {
stopTracking()
} else {
startTracking()
}
}
private func startTracking() {
locationTrackingCancellation = mapView.location.onLocationChange.observe { [weak mapView] newLocation in
guard let location = newLocation.last, let mapView else { return }
mapView.camera.ease(
to: CameraOptions(center: location.coordinate, zoom: 15),
duration: 1.3)
}
if #available(iOS 13.0, *) {
trackingButton.setImage(UIImage(systemName: "location.fill"), for: .normal)
} else {
trackingButton.setTitle("No tracking", for: .normal)
}
}
func stopTracking() {
if #available(iOS 13.0, *) {
trackingButton.setImage(UIImage(systemName: "location"), for: .normal)
} else {
trackingButton.setTitle("Track", for: .normal)
}
locationTrackingCancellation = nil
}
func addStyleToggle() {
// Create a UISegmentedControl to toggle between map styles
styleToggle.selectedSegmentIndex = style.rawValue
styleToggle.addTarget(self, action: #selector(switchStyle(sender:)), for: .valueChanged)
styleToggle.translatesAutoresizingMaskIntoConstraints = false
// set the segmented control as the title view
navigationItem.titleView = styleToggle
}
}
extension ViewController: GestureManagerDelegate {
public func gestureManager(_ gestureManager: MapboxMaps.GestureManager, didBegin gestureType: MapboxMaps.GestureType) {
stopTracking()
}
public func gestureManager(_ gestureManager: MapboxMaps.GestureManager, didEnd gestureType: MapboxMaps.GestureType, willAnimate: Bool) {}
public func gestureManager(_ gestureManager: MapboxMaps.GestureManager, didEndAnimatingFor gestureType: MapboxMaps.GestureType) {}
}
extension ViewController {
private enum Style: Int, CaseIterable {
var name: String {
switch self {
case .standard:
return "Standard"
case .light:
return "Light"
case .satelliteStreets:
return "Satellite"
case .customUri:
return "Custom"
}
}
var uri: StyleURI {
switch self {
case .standard:
return .standard
case .light:
return .light
case .satelliteStreets:
return .satelliteStreets
case .customUri:
let localStyleURL = Bundle.main.url(forResource: "blueprint_style", withExtension: "json")!
return .init(url: localStyleURL)!
}
}
case standard
case light
case satelliteStreets
case customUri
}
}