Back to examples
advanced

Customize the user location annotation

Override the default user location annotation.

      

import Mapbox

class ViewController: UIViewController, MGLMapViewDelegate {
    let point = MGLPointAnnotation()

    override func viewDidLoad() {
        super.viewDidLoad()
        let mapView = MGLMapView(frame: view.bounds)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.delegate = self
        
        // Enable heading tracking mode so that the arrow will appear.
        mapView.userTrackingMode = .followWithHeading

        // Enable the permanent heading indicator, which will appear when the tracking mode is not `.followWithHeading`.
        mapView.showsUserHeadingIndicator = true

        view.addSubview(mapView)
    }
    
    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        // Substitute our custom view for the user location annotation. This custom view is defined below.
        if annotation is MGLUserLocation && mapView.userLocation != nil {
            return CustomUserLocationAnnotationView()
        }
        return nil
    }

    // Optional: tap the user location annotation to toggle heading tracking mode.
    func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
        if mapView.userTrackingMode != .followWithHeading {
            mapView.userTrackingMode = .followWithHeading
        } else {
            mapView.resetNorth()
        }

        // We're borrowing this method as a gesture recognizer, so reset selection state.
        mapView.deselectAnnotation(annotation, animated: false)
    }
}


// Create a subclass of MGLUserLocationAnnotationView.
class CustomUserLocationAnnotationView: MGLUserLocationAnnotationView {
    let size: CGFloat = 48
    var dot: CALayer!
    var arrow: CAShapeLayer!
    
    // -update is a method inherited from MGLUserLocationAnnotationView. It updates the appearance of the user location annotation when needed. This can be called many times a second, so be careful to keep it lightweight.
    override func update() {
        if frame.isNull {
            frame = CGRect(x: 0, y: 0, width: size, height: size)
            return setNeedsLayout()
        }

        // Check whether we have the user’s location yet.
        if CLLocationCoordinate2DIsValid(userLocation!.coordinate) {
            setupLayers()
            updateHeading()
        }
    }
    
    private func updateHeading() {
        // Show the heading arrow, if the heading of the user is available.
        if let heading = userLocation!.heading?.trueHeading {
            arrow.isHidden = false

            // Get the difference between the map’s current direction and the user’s heading, then convert it from degrees to radians.
            let rotation: CGFloat = -MGLRadiansFromDegrees(mapView!.direction - heading)
            
            // If the difference would be perceptible, rotate the arrow.
            if fabs(rotation) > 0.01 {
                // Disable implicit animations of this rotation, which reduces lag between changes.
                CATransaction.begin()
                CATransaction.setDisableActions(true)
                arrow.setAffineTransform(CGAffineTransform.identity.rotated(by: rotation))
                CATransaction.commit()
            }
        } else {
            arrow.isHidden = true
        }
    }
    
    private func setupLayers() {
        // This dot forms the base of the annotation.
        if dot == nil {
            dot = CALayer()
            dot.bounds = CGRect(x: 0, y: 0, width: size, height: size)

            // Use CALayer’s corner radius to turn this layer into a circle.
            dot.cornerRadius = size / 2
            dot.backgroundColor = super.tintColor.cgColor
            dot.borderWidth = 4
            dot.borderColor = UIColor.white.cgColor
            layer.addSublayer(dot)
        }

        // This arrow overlays the dot and is rotated with the user’s heading.
        if arrow == nil {
            arrow = CAShapeLayer()
            arrow.path = arrowPath()
            arrow.frame = CGRect(x: 0, y: 0, width: size / 2, height: size / 2)
            arrow.position = CGPoint(x: dot.frame.midX, y: dot.frame.midY)
            arrow.fillColor = dot.borderColor
            layer.addSublayer(arrow)
        }
    }

    // Calculate the vector path for an arrow, for use in a shape layer.
    private func arrowPath() -> CGPath {
        let max: CGFloat = size / 2
        let pad: CGFloat = 3
        
        let top =    CGPoint(x: max * 0.5, y: 0)
        let left =   CGPoint(x: 0 + pad,   y: max - pad)
        let right =  CGPoint(x: max - pad, y: max - pad)
        let center = CGPoint(x: max * 0.5, y: max * 0.6)

        let bezierPath = UIBezierPath()
        bezierPath.move(to: top)
        bezierPath.addLine(to: left)
        bezierPath.addLine(to: center)
        bezierPath.addLine(to: right)
        bezierPath.addLine(to: top)
        bezierPath.close()

        return bezierPath.cgPath
    }
}




      
      


#import "ViewController.h"

@import Mapbox;

// Create a subclass of MGLUserLocationAnnotationView.
@interface CustomUserLocationAnnotationView : MGLUserLocationAnnotationView

@property (nonatomic) CGFloat size;
@property (nonatomic) CALayer *dot;
@property (nonatomic) CAShapeLayer *arrow;

@end

@implementation CustomUserLocationAnnotationView

- (instancetype)init {
    self.size = 48;
    self = [super initWithFrame:CGRectMake(0, 0, self.size, self.size)];
    
    return self;
}

// -update is a method inherited from MGLUserLocationAnnotationView. It updates the appearance of the user location annotation when needed. This can be called many times a second, so be careful to keep it lightweight.
- (void)update {
    // Check whether we have the user’s location yet.
    if (CLLocationCoordinate2DIsValid(self.userLocation.coordinate)) {
        [self setupLayers];
        [self updateHeading];
    }
}

- (void)updateHeading {
    // Show the heading arrow, if the heading of the user is available.
    if (self.userLocation.heading.trueHeading) {
        _arrow.hidden = NO;
        
        // Get the difference between the map’s current direction and the user’s heading, then convert it from degrees to radians.
        CGFloat rotation = -MGLRadiansFromDegrees(self.mapView.direction - self.userLocation.heading.trueHeading);

        // If the difference would be perceptible, rotate the arrow.
        if (fabs(rotation) > 0.01) {
            // Disable implicit animations of this rotation, which reduces lag between changes.
            [CATransaction begin];
            [CATransaction setDisableActions:YES];
            _arrow.affineTransform = CGAffineTransformRotate(CGAffineTransformIdentity, rotation);
            [CATransaction commit];
        }
    } else {
        _arrow.hidden = YES;
    }
}

- (void)setupLayers {
    // This dot forms the base of the annotation.
    if (!_dot) {
        _dot = [CALayer layer];
        _dot.frame = self.bounds;

        // Use CALayer’s corner radius to turn this layer into a circle.
        _dot.cornerRadius = _size / 2;
        _dot.backgroundColor = super.tintColor.CGColor;
        _dot.borderWidth = 4;
        _dot.borderColor = [UIColor whiteColor].CGColor;

        [self.layer addSublayer:_dot];
    }

    // This arrow overlays the dot and is rotated with the user’s heading.
    if (!_arrow) {
        _arrow = [CAShapeLayer layer];
        _arrow.path = [self arrowPath];
        _arrow.frame = CGRectMake(0, 0, _size / 2, _size / 2);
        _arrow.position = CGPointMake(CGRectGetMidX(_dot.frame), CGRectGetMidY(_dot.frame));
        _arrow.fillColor = _dot.borderColor;
        [self.layer addSublayer:_arrow];
    }
}

// Calculate the vector path for an arrow, for use in a shape layer.
- (CGPathRef)arrowPath {
    CGFloat max = _size / 2;
    CGFloat pad = 3;
    
    CGPoint top =    CGPointMake(max * 0.5, 0);
    CGPoint left =   CGPointMake(0 + pad,   max - pad);
    CGPoint right =  CGPointMake(max - pad, max - pad);
    CGPoint center = CGPointMake(max * 0.5, max * 0.6);
    
    UIBezierPath *bezierPath = [UIBezierPath bezierPath];
    [bezierPath moveToPoint:top];
    [bezierPath addLineToPoint:left];
    [bezierPath addLineToPoint:center];
    [bezierPath addLineToPoint:right];
    [bezierPath addLineToPoint:top];
    [bezierPath closePath];
    
    return bezierPath.CGPath;
}

@end


@interface ViewController () <MGLMapViewDelegate>
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MGLMapView *mapView = [[MGLMapView alloc] initWithFrame:self.view.bounds];
    mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    mapView.delegate = self;
    
    // Enable heading tracking mode so that the arrow will appear.
    mapView.userTrackingMode = MGLUserTrackingModeFollowWithHeading;

    // Enable the permanent heading indicator, which will appear when the tracking mode is not `MGLUserTrackingModeFollowWithHeading`.
    mapView.showsUserHeadingIndicator = YES;
    
    [self.view addSubview:mapView];
}

- (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id<MGLAnnotation>)annotation {
    // Substitute our custom view for the user location annotation. This custom view is defined above.
    if ([annotation isKindOfClass:[MGLUserLocation class]]) {
        return [[CustomUserLocationAnnotationView alloc] init];
    }
    
    return nil;
}

// Optional: tap the user location annotation to toggle heading tracking mode.
- (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id<MGLAnnotation>)annotation {
    if (mapView.userTrackingMode != MGLUserTrackingModeFollowWithHeading) {
        mapView.userTrackingMode = MGLUserTrackingModeFollowWithHeading;
    } else {
        [mapView resetNorth];
    }

    // We're borrowing this method as a gesture recognizer, so reset selection state.
    [mapView deselectAnnotation:annotation animated:NO];
}

@end