Z-index of iOS MapKit user location annotation
Asked Answered
P

9

38

I need to draw the current user annotation (the blue dot) on top of all other annotations. Right now it is getting drawn underneath my other annotations and getting hidden. I'd like to adjust the z-index of this annotation view (the blue dot) and bring it to the top, does anyone know how?

Update: So I can now get a handle on the MKUserLocationView, but how do I bring it forward?

- (void) mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views {
    for (MKAnnotationView *view in views) {
        if ([[view annotation] isKindOfClass:[MKUserLocation class]]) {
            // How do I bring this view to the front of all other annotations?
            // [???? bringSubviewToFront:????];
        }
    }
}
Puebla answered 22/8, 2011 at 1:30 Comment(0)
H
12

Update for iOS 14

I know it's an old post, but the question is still applicable and you end up here when typing it into your favorite search engine.

Starting with iOS 14, Apple introduced a zPriority property to MKAnnotationView. You can use it to set up the z-index for your annotations using predefined constants or floats. Also, Apple made it possible to finally create the view for the user location on our own and provided MKUserLocationView as a subclass of MKAnnotationView.

From the documentation for MKUserLocationView:

If you want to specify additional configuration, such as zPriority, create this annotation view directly. To display the annotation view, return the instance from mapView(_:viewFor:).

The following code snippet shows how this can be done (add the code to your MKMapViewDelegate):

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    
    // Alter the MKUserLocationView (iOS 14+)
    if #available(iOS 14.0, *), annotation is MKUserLocation {
        
        // Try to reuse the existing view that we create below
        let reuseIdentifier = "userLocation"
        if let existingView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) {
            return existingView
        }
        let view = MKUserLocationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
        
        view.zPriority = .max   // Show user location above other annotations
        view.isEnabled = false  // Ignore touch events and do not show callout
        
        return view
    }
    
    // Create views for other annotations or return nil to use the default representation
    
    return nil
}

Note that per default, the user location annotation shows a callout when tapping on it. Now that the user location overlays your other annotations, you'd probably want to disable this, which is done in the code by setting .isEnabled to false.

Headword answered 12/11, 2020 at 11:4 Comment(1)
I see this has no comments, but this is the only thing that worked for me with iOS 16. Thanks!Prolamine
P
34

Finally got it to work using the code listed below thanks to the help from Paul Tiarks. The problem I ran into is that the MKUserLocation annotation gets added to the map first before any others, so when you add the other annotations their order appears to be random and would still end up on top of the MKUserLocation annotation. To fix this I had to move all the other annotations to the back as well as move the MKUserLocation annotation to the front.

- (void) mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views 
{
    for (MKAnnotationView *view in views) 
    {
        if ([[view annotation] isKindOfClass:[MKUserLocation class]]) 
        {
            [[view superview] bringSubviewToFront:view];
        } 
        else 
        {
            [[view superview] sendSubviewToBack:view];
        }
    }
}

Update: You may want to add the code below to ensure the blue dot is drawn on top when scrolling it off the viewable area of the map.

- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{        
  for (NSObject *annotation in [mapView annotations]) 
  {
    if ([annotation isKindOfClass:[MKUserLocation class]]) 
    {
      NSLog(@"Bring blue location dot to front");
      MKAnnotationView *view = [mapView viewForAnnotation:(MKUserLocation *)annotation];
      [[view superview] bringSubviewToFront:view];
    }
  }
}
Puebla answered 29/8, 2011 at 5:4 Comment(3)
This solution doesn't seem to be reliable to me. The blue location dot ends up underneath my custom annotations if I drag the map so it is offscreen and return to it, presumably because the map view redraws the views with their original layout.Hypotenuse
@DanJ Then use the same logic inside - (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated and loop through mapView.annotations and find [mapView viewForAnnotation:annotation]Puebla
Good point, that does solve the problem, although I found putting the code in regionDidChangeAnimated more reliable than putting it in regionWillChangeAnimated. I'll update your answer to include the extra code. FTR, there is a slight performance concern when large numbers of annotations are used as the region*ChangeAnimated methods should be kept as lightweight as possible.Hypotenuse
B
26

Another solution: setup annotation view layer's zPosition (annotationView.layer.zPosition) in:

- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views;

Burne answered 3/5, 2013 at 11:7 Comment(10)
this solution occured the most reliable for me.Venous
This if far easier/cleaner than managing the order of subviews.Nitrogenize
This makes the pin cover callouts from other pinsPublicize
I got around the covering callouts problem by toggling the z position on - mapView:didSelectAnnotationView: and mapView:didDeselectAnnotationView:Burchette
yuf, see comment belowPiperine
The layer's Z-index can be set when creating the view in mapView:viewForAnnotation: instead of mapView:didAddAnnotationViews:Creedon
I'm guessing the default zPosition is 0. I used 3 for the markers I wanted on top and it worked. The other answers where not reliable. This is much better. Thanks!Very
Thanks. This worked well for me: func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { views.first { $0.annotation is MKUserLocation }?.layer.zPosition = 99 }Maziar
Unfortunately on iOS 11 this does not work for me. Also other solutions in this thread do not work for me on iOS 11.Ski
No solution worked for me either. The little blue dot likes hiding behind other markers, and it seems there is no way to get it in the foreground. Half a day wasted. Useless comment, I know, but comes out of pure frustration. Up-voted the question, anyway, which is surely meaningful.Sequacious
P
12

The official answer to that thread is wrong... using zPosition is indeed the best approach and fastest vs using regionDidChangeAnimated...

else you would suffer big performance impact with many annotations on map (as every change of frame would rescan all annotations). and been testing it...

so when creating the view of the annotation (or in didAddAnnotationViews) set : self.layer.zPosition = -1; (below all others)

and as pointed out by yuf: This makes the pin cover callouts from other pins – yuf Dec 5 '13 at 20:25

i.e. the annotation view will appear below other pins.

to fix, simply reput the zPosition to 0 when you have a selection

-(void) mapView:(MKMapView*)mapView didSelectAnnotationView:(MKAnnotationView*)view {
    if ([view isKindOfClass:[MyCustomAnnotationView class]])
        view.layer.zPosition = 0;
   ...
}

-(void) mapView:(MKMapView*)mapView didDeselectAnnotationView:(MKAnnotationView*)view {
    if ([view isKindOfClass:[MyCustomAnnotationView class]])
        view.layer.zPosition = -1;
   ...
}
Piperine answered 22/9, 2014 at 20:45 Comment(2)
This solution is nice when you just have to manage the visual, but if you need to handle tap/touch priorities too, it doesn't work (you tap on a pin that seems to be on top of the others (because zPosition is 0), but you actually tap on a pin below).Adah
this is the standard pin behavior with apple... changing zlocation doesn't prevent this... but this is another issue. for not popping up, you should use self.canShowCallout = NO in your custom annotationPiperine
H
12

Update for iOS 14

I know it's an old post, but the question is still applicable and you end up here when typing it into your favorite search engine.

Starting with iOS 14, Apple introduced a zPriority property to MKAnnotationView. You can use it to set up the z-index for your annotations using predefined constants or floats. Also, Apple made it possible to finally create the view for the user location on our own and provided MKUserLocationView as a subclass of MKAnnotationView.

From the documentation for MKUserLocationView:

If you want to specify additional configuration, such as zPriority, create this annotation view directly. To display the annotation view, return the instance from mapView(_:viewFor:).

The following code snippet shows how this can be done (add the code to your MKMapViewDelegate):

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    
    // Alter the MKUserLocationView (iOS 14+)
    if #available(iOS 14.0, *), annotation is MKUserLocation {
        
        // Try to reuse the existing view that we create below
        let reuseIdentifier = "userLocation"
        if let existingView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) {
            return existingView
        }
        let view = MKUserLocationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
        
        view.zPriority = .max   // Show user location above other annotations
        view.isEnabled = false  // Ignore touch events and do not show callout
        
        return view
    }
    
    // Create views for other annotations or return nil to use the default representation
    
    return nil
}

Note that per default, the user location annotation shows a callout when tapping on it. Now that the user location overlays your other annotations, you'd probably want to disable this, which is done in the code by setting .isEnabled to false.

Headword answered 12/11, 2020 at 11:4 Comment(1)
I see this has no comments, but this is the only thing that worked for me with iOS 16. Thanks!Prolamine
O
3

Just use the .layer.anchorPointZ property.

Example:

 func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
        views.forEach {
            if let _ = $0.annotation as? MKUserLocation {
                $0.layer.anchorPointZ = 0
            } else {
                $0.layer.anchorPointZ = 1
            }
        }
    }

Here is there reference https://developer.apple.com/documentation/quartzcore/calayer/1410796-anchorpointz

Oodles answered 22/12, 2019 at 13:20 Comment(1)
great! But note, everyone, resulting order is opposite to zPosition. I.e. layer.anchorPointZ =0 is on top, 100 is below.Pasadena
H
2

Try, getting a reference to the user location annotation (perhaps in mapView: didAddAnnotationViews:) and then bring that view to the front of the mapView after all of your annotations have been added.

Hienhieracosphinx answered 22/8, 2011 at 1:51 Comment(6)
I haven't figured out to get a handle on it because you can't test for its class MKUserLocationView and all of my other annotations are regular MKAnnotationView objects and MKUserLocationView is a subclass of MKAnnotationView... I'm thinking about switching over my annotations to overlays so that I can manage the zindex of those and keep them separate from the user location annotation. Any other thoughts?Puebla
You should be able to test the annotation property's class to see if it is an MKUserLocation and keep that as your user location view.Hienhieracosphinx
Thanks to your suggestion above I've been able to get ahold of the MKUserLocationView, but I haven't been able to bring it forward. I've tried various incantations with the bringSubviewToFront: but nothing I've tried works. Any thoughts?Puebla
you may need to ask the superview of your MKUserLocationView to bring it to the front. I'm sure MKMapView does some unpredictable view hierarchies under the covers, but directly asking the MKUserLocationView's superview to bring it to front should do the trick, worth a shot anyway.Hienhieracosphinx
Expanding on Paul's comment above, have you tried [[view superview] bringSubviewToFront:view];?Obala
I tried [[view superview] bringSubviewToFront:view]; and it doesn't work.Puebla
D
1

Swift 3:

internal func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
        for annotationView in views {
            if annotationView.annotation is MKUserLocation {
                annotationView.bringSubview(toFront: view)
                return
            }
            annotationView.sendSubview(toBack: view)
        }
 }
Decentralization answered 13/2, 2017 at 20:24 Comment(0)
H
0

Here is a way to do it using predicates. I think it should be faster

NSPredicate *userLocationPredicate = [NSPredicate predicateWithFormat:@"class == %@", [MKUserLocation class]];
NSArray* userLocation = [[self.mapView annotations] filteredArrayUsingPredicate:userLocationPredicate];

if([userLocation count]) {
    NSLog(@"Bring blue location dot to front");
    MKAnnotationView *view = [self.mapView viewForAnnotation:(MKUserLocation *)[userLocation objectAtIndex:0]];
    [[view superview] bringSubviewToFront:view];
}
Horrify answered 6/3, 2013 at 20:25 Comment(0)
F
0

Using Underscore.m

_.array(mapView.annotations).
    filter(^ BOOL (id<MKAnnotation> annotation) { return [annotation 
isKindOfClass:[MKUserLocation class]]; })
    .each(^ (id<MKAnnotation> annotation) { [[[mapView 
viewForAnnotation:annotation] superview] bringSubviewToFront:[mapView 
viewForAnnotation:annotation]]; });
Feisty answered 15/7, 2014 at 13:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.