Switch between user tracking modes
A newer version of the Maps SDK is available
This page uses v6.4.1 of the Mapbox Maps SDK. A newer version of the SDK is available. Learn about the latest version, v11.8.0, in the Maps SDK documentation.
This example uses two classes within a single file:
UserLocationButton
is a subclass ofUIButton
that updates its style when the tracking mode state changes.ViewController
creates theMGLMapView
and allows the user to toggle betweenMGLUserTrackingMode
s using theUserLocationButton
.
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
var mapView: MGLMapView!
var userLocationButton: UserLocationButton?
override func viewDidLoad() {
super.viewDidLoad()
mapView = MGLMapView(frame: view.bounds, styleURL: MGLStyle.darkStyleURL)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.delegate = self
// The user location annotation takes its color from the map view's tint color.
mapView.tintColor = .red
mapView.attributionButton.tintColor = .lightGray
// Enable the always-on heading indicator for the user location annotation.
mapView.showsUserHeadingIndicator = true
view.addSubview(mapView)
// Create button to allow user to change the tracking mode.
setupLocationButton()
}
// Update the button state when the user tracking mode updates or resets.
func mapView(_ mapView: MGLMapView, didChange mode: MGLUserTrackingMode, animated: Bool) {
guard let userLocationButton = userLocationButton else { return }
userLocationButton.updateArrowForTrackingMode(mode: mode)
}
// Update the user tracking mode when the user toggles through the
// user tracking mode button.
@IBAction func locationButtonTapped(sender: UserLocationButton) {
var mode: MGLUserTrackingMode
switch (mapView.userTrackingMode) {
case .none:
mode = .follow
case .follow:
mode = .followWithHeading
case .followWithHeading:
mode = .followWithCourse
case .followWithCourse:
mode = .none
@unknown default:
fatalError("Unknown user tracking mode")
}
mapView.userTrackingMode = mode
}
// Button creation and autolayout setup
func setupLocationButton() {
let userLocationButton = UserLocationButton(buttonSize: 80)
userLocationButton.addTarget(self, action: #selector(locationButtonTapped), for: .touchUpInside)
userLocationButton.tintColor = mapView.tintColor
// Setup constraints such that the button is placed within
// the upper left corner of the view.
userLocationButton.translatesAutoresizingMaskIntoConstraints = false
var leadingConstraintSecondItem: AnyObject
if #available(iOS 11.0, *) {
leadingConstraintSecondItem = view.safeAreaLayoutGuide
} else {
leadingConstraintSecondItem = view
}
let constraints: [NSLayoutConstraint] = [
NSLayoutConstraint(item: userLocationButton, attribute: .top, relatedBy: .greaterThanOrEqual, toItem: topLayoutGuide, attribute: .bottom, multiplier: 1, constant: 10),
NSLayoutConstraint(item: userLocationButton, attribute: .leading, relatedBy: .equal, toItem: leadingConstraintSecondItem, attribute: .leading, multiplier: 1, constant: 10),
NSLayoutConstraint(item: userLocationButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: userLocationButton.frame.size.height),
NSLayoutConstraint(item: userLocationButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: userLocationButton.frame.size.width)
]
view.addSubview(userLocationButton)
view.addConstraints(constraints)
self.userLocationButton = userLocationButton
}
}
// MARK: - Custom UIButton subclass
class UserLocationButton: UIButton {
private var arrow: CAShapeLayer?
private let buttonSize: CGFloat
// Initializer to create the user tracking mode button
init(buttonSize: CGFloat) {
self.buttonSize = buttonSize
super.init(frame: CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize))
self.backgroundColor = UIColor.white.withAlphaComponent(0.9)
self.layer.cornerRadius = 4
let arrow = CAShapeLayer()
arrow.path = arrowPath()
arrow.lineWidth = 2
arrow.lineJoin = CAShapeLayerLineJoin.round
arrow.bounds = CGRect(x: 0, y: 0, width: buttonSize / 2, height: buttonSize / 2)
arrow.position = CGPoint(x: buttonSize / 2, y: buttonSize / 2)
arrow.shouldRasterize = true
arrow.rasterizationScale = UIScreen.main.scale
arrow.drawsAsynchronously = true
self.arrow = arrow
// Update arrow for initial tracking mode
updateArrowForTrackingMode(mode: .none)
layer.addSublayer(self.arrow!)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Create a new bezier path to represent the tracking mode arrow,
// making sure the arrow does not get drawn outside of the
// frame size of the UIButton.
private func arrowPath() -> CGPath {
let bezierPath = UIBezierPath()
let max: CGFloat = buttonSize / 2
bezierPath.move(to: CGPoint(x: max * 0.5, y: 0))
bezierPath.addLine(to: CGPoint(x: max * 0.1, y: max))
bezierPath.addLine(to: CGPoint(x: max * 0.5, y: max * 0.65))
bezierPath.addLine(to: CGPoint(x: max * 0.9, y: max))
bezierPath.addLine(to: CGPoint(x: max * 0.5, y: 0))
bezierPath.close()
return bezierPath.cgPath
}
// Update the arrow's color and rotation when tracking mode is changed.
func updateArrowForTrackingMode(mode: MGLUserTrackingMode) {
let activePrimaryColor = UIColor.red
let disabledPrimaryColor = UIColor.clear
let disabledSecondaryColor = UIColor.black
let rotatedArrow = CGFloat(0.66)
switch mode {
case .none:
updateArrow(fillColor: disabledPrimaryColor, strokeColor: disabledSecondaryColor, rotation: 0)
case .follow:
updateArrow(fillColor: disabledPrimaryColor, strokeColor: activePrimaryColor, rotation: 0)
case .followWithHeading:
updateArrow(fillColor: activePrimaryColor, strokeColor: activePrimaryColor, rotation: rotatedArrow)
case .followWithCourse:
updateArrow(fillColor: activePrimaryColor, strokeColor: activePrimaryColor, rotation: 0)
@unknown default:
fatalError("Unknown user tracking mode")
}
}
func updateArrow(fillColor: UIColor, strokeColor: UIColor, rotation: CGFloat) {
guard let arrow = arrow else { return }
arrow.fillColor = fillColor.cgColor
arrow.strokeColor = strokeColor.cgColor
arrow.setAffineTransform(CGAffineTransform.identity.rotated(by: rotation))
// Re-center the arrow within the button if rotated
if rotation > 0 {
arrow.position = CGPoint(x: buttonSize / 2 + 2, y: buttonSize / 2 - 2)
}
layoutIfNeeded()
}
}
#import "ViewController.h"
@import Mapbox;
#pragma mark - UIButton subclass
// Subclass UIButton to create a custom user tracking mode button
@interface UserLocationButton : UIButton
@property (nonatomic) CAShapeLayer *arrow;
@property (nonatomic) CGFloat buttonSize;
@end
@implementation UserLocationButton
// Initializer to create the user tracking mode button
- (instancetype)initWithButtonSize:(CGFloat)buttonSize {
if (self = [super init]) {
self.frame = CGRectMake(0, 0, buttonSize, buttonSize);
self.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9f];
self.layer.cornerRadius = 4;
self.buttonSize = buttonSize;
CAShapeLayer *arrow = [[CAShapeLayer alloc] init];
arrow.path = [self arrowPath];
arrow.lineWidth = 2;
arrow.lineJoin = kCALineJoinRound;
arrow.bounds = CGRectMake(0, 0, buttonSize / 2, buttonSize / 2);
arrow.position = CGPointMake(buttonSize / 2, buttonSize / 2);
arrow.shouldRasterize = YES;
arrow.rasterizationScale = [[UIScreen mainScreen] scale];
arrow.drawsAsynchronously = YES;
self.arrow = arrow;
// Update arrow for initial tracking mode
[self updateArrowForTrackingMode:MGLUserTrackingModeNone];
[self.layer addSublayer:self.arrow];
}
return self;
}
// Create a new bezier path to represent the tracking mode arrow,
// making sure the arrow does not get drawn outside of the
// frame size of the UIButton.
- (CGPathRef)arrowPath {
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
CGFloat max = self.buttonSize / 2;
[bezierPath moveToPoint:CGPointMake(max * 0.5f, 0)];
[bezierPath addLineToPoint:CGPointMake(max * 0.1f, max)];
[bezierPath addLineToPoint:CGPointMake(max * 0.5f, max * 0.65f)];
[bezierPath addLineToPoint:CGPointMake(max * 0.9f, max)];
[bezierPath addLineToPoint:CGPointMake(max * 0.5f, 0)];
[bezierPath closePath];
return bezierPath.CGPath;
}
// Update the arrow's color and rotation when tracking mode is changed
- (void)updateArrowForTrackingMode:(MGLUserTrackingMode)mode {
UIColor *activePrimaryColor = UIColor.redColor;
UIColor *disabledPrimaryColor = UIColor.clearColor;
UIColor *disabledSecondaryColor = UIColor.blackColor;
CGFloat rotatedArrow = 0.66f;
switch (mode) {
case MGLUserTrackingModeNone:
[self updateArrowFillColor:disabledPrimaryColor
strokeColor:disabledSecondaryColor
rotation:0];
break;
case MGLUserTrackingModeFollow:
[self updateArrowFillColor:disabledPrimaryColor
strokeColor:activePrimaryColor
rotation:0];
break;
case MGLUserTrackingModeFollowWithHeading:
[self updateArrowFillColor:activePrimaryColor
strokeColor:activePrimaryColor
rotation:rotatedArrow];
break;
case MGLUserTrackingModeFollowWithCourse:
[self updateArrowFillColor:activePrimaryColor
strokeColor:activePrimaryColor
rotation:0];
break;
}
}
- (void)updateArrowFillColor:(UIColor*)fillColor strokeColor:(UIColor*) strokeColor rotation:(CGFloat) rotation {
self.arrow.fillColor = fillColor.CGColor;
self.arrow.strokeColor = strokeColor.CGColor;
self.arrow.affineTransform = CGAffineTransformMakeRotation(rotation);
// Re-center the arrow within the button if rotated
if (rotation > 0) {
self.arrow.position = CGPointMake(self.buttonSize / 2 + 2, self.buttonSize / 2 - 2);
}
[self layoutIfNeeded];
}
@end
#pragma mark - ViewController
@interface ViewController () <MGLMapViewDelegate>
@property (nonatomic) MGLMapView *mapView;
@property (nonatomic) UserLocationButton *userLocationButton;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.mapView = [[MGLMapView alloc] initWithFrame:self.view.bounds styleURL:[MGLStyle darkStyleURL]];
self.mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.mapView.delegate = self;
// The user location annotation takes its color from the map view's tint color.
self.mapView.tintColor = [UIColor redColor];
self.mapView.attributionButton.tintColor = [UIColor lightGrayColor];
// Enable the always-on heading indicator for the user location annotation.
self.mapView.showsUserHeadingIndicator = YES;
[self.view addSubview:self.mapView];
// Create button to allow user to change the tracking mode.
[self setupLocationButton];
}
// Update the button state when the user tracking mode updates or resets.
- (void)mapView:(MGLMapView *)mapView didChangeUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated {
[self.userLocationButton updateArrowForTrackingMode:mode];
}
// Update the user tracking mode when the user toggles through the
// user tracking mode button.
- (void)locationButtonTapped:(UserLocationButton *)sender {
MGLUserTrackingMode mode;
switch (self.mapView.userTrackingMode) {
case MGLUserTrackingModeNone:
mode = MGLUserTrackingModeFollow;
break;
case MGLUserTrackingModeFollow:
mode = MGLUserTrackingModeFollowWithHeading;
break;
case MGLUserTrackingModeFollowWithHeading:
mode = MGLUserTrackingModeFollowWithCourse;
break;
case MGLUserTrackingModeFollowWithCourse:
mode = MGLUserTrackingModeNone;
break;
}
[self.mapView setUserTrackingMode:mode];
}
// Button creation and autolayout setup
- (void)setupLocationButton {
UserLocationButton *userLocationButton = [[UserLocationButton alloc] initWithButtonSize:80];
[userLocationButton addTarget:self action:@selector(locationButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
userLocationButton.tintColor = self.mapView.tintColor;
// Setup constraints such that the button is placed within
// the upper left corner of the view.
userLocationButton.translatesAutoresizingMaskIntoConstraints = NO;
id leadingConstraintSecondItem;
if (@available(iOS 11.0, *)) {
leadingConstraintSecondItem = self.view.safeAreaLayoutGuide;
} else {
leadingConstraintSecondItem = self.view;
}
NSArray<NSLayoutConstraint *> *constraints = @[
[NSLayoutConstraint constraintWithItem:userLocationButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom multiplier:1 constant:10],
[NSLayoutConstraint constraintWithItem:userLocationButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:leadingConstraintSecondItem attribute:NSLayoutAttributeLeading multiplier:1 constant:10],
[NSLayoutConstraint constraintWithItem:userLocationButton attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:userLocationButton.frame.size.height],
[NSLayoutConstraint constraintWithItem:userLocationButton attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:userLocationButton.frame.size.width]
];
[self.view addSubview:userLocationButton];
[self.view addConstraints:constraints];
self.userLocationButton = userLocationButton;
}
@end