How to define the order of overlapping MKAnnotationViews?
Asked Answered
V

7

40

I have several MKAnnotations (and their corresponding views) in my map, and it sometimes gets really crowded. Now, the annotations in my app come in two flavors: some are bound to stay where they are, while others will move as time goes on. I'd prefer to have the more stable ones visually in the background and the moving ones to always pass in front of them.

One would think, perhaps, that the annotations most recently added to the map would end up to the front (or alternatively at the very back, at least) but this just doesn't seem to be the rule. As far as I can tell, I create and add ALL the non-moving annotations first, and then add some newly-instantiated moving annotations, but many of them (although not all!) end up drawn under the perpetually stock-still ones.

Interestingly, when time goes by, and yet new moving annotations are created, they tend to gravitate more to the top than the first ones - even if all moving annotation objects were created only after the nonmoving parts were already added to the map.

Does anyone know a trick to alter this strange natural order of the annotation views on the map? I tried to search the Map Kit API, but it doesn't seem to speak of such a thing.

Voile answered 17/7, 2009 at 19:33 Comment(1)
I also need a solution to this problem. Did you find your answer?Zenger
S
41

Ok, so for solution use method from MKMapViewDelegate


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

In this method you should rearrange AnnotationView after it was added to mapKit View. So, code may looks like this:


- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
   for (MKAnnotationView * annView in views) {
      TopBottomAnnotation * ann = (TopBottomAnnotation *) [annView annotation];
      if ([ann top]) {
         [[annView superview] bringSubviewToFront:annView];
      } else {
         [[annView superview] sendSubviewToBack:annView];
      }
   }

}

This works for me.

Schild answered 31/7, 2009 at 16:9 Comment(8)
+1: I also use bringSubviewToFront:. Although, I don't need anything as fancy as the above.Thibodeaux
Thanks, tt.Kilew, that's exactly the idea I was looking for.Voile
To fix the callout ordering, you can use KVO (or mapView:didSelectAnnotationView: on iOS4) to detect when the annotation was selected and bring it to front. If the annotation was deselected, you just run the same z-ordering logic to put it back in place. Worked perfectly for me. blog.evandavey.com/2009/07/…Existential
Where does the TopBottomAnnotation class come from?Blink
It is your custom annotation class, that knows if it should be moved on top, or should be send to backSchild
You also need to check if the annotationview happens to be to users current location. doing [annview annotation] will return a MKUserLocation if its the users current location, which will not be an instance of your "TopBottomAnnotation" which will cause the app to crash. Do if ([annView.annotation isKindOfClass:[TopBottomAnnotation class]]) ...Kuehl
@Sanjuju it's a method that will return if your AnnotationThatCanBeTopPlacedOrBottomPlaced should be placed on top or on bottomSchild
What if I need to place annotations of type #1 on top, type #2 under the #1 and type #3 under #2? I need cross iOS 10, 11 and 12 working solution...Mho
E
28

Under iOS 11 the implementation of displayPriority broke all the solutions which use bringSubviewToFront or zPosition.

If you override the annotation view's CALayer, you can wrestle control of zPosition back from the OS.

class AnnotationView: MKAnnotationView {

    /// Override the layer factory for this class to return a custom CALayer class
    override class var layerClass: AnyClass {
        return ZPositionableLayer.self
    }

    /// convenience accessor for setting zPosition
    var stickyZPosition: CGFloat {
        get {
            return (self.layer as! ZPositionableLayer).stickyZPosition
        }
        set {
            (self.layer as! ZPositionableLayer).stickyZPosition = newValue
        }
    }

    /// force the pin to the front of the z-ordering in the map view
   func bringViewToFront() {
        superview?.bringSubviewToFront(toFront: self)
        stickyZPosition = CGFloat(1)
    }

    /// force the pin to the back of the z-ordering in the map view
   func setViewToDefaultZOrder() {
        stickyZPosition = CGFloat(0)
    }

}

/// iOS 11 automagically manages the CALayer zPosition, which breaks manual z-ordering.
/// This subclass just throws away any values which the OS sets for zPosition, and provides
/// a specialized accessor for setting the zPosition
private class ZPositionableLayer: CALayer {

    /// no-op accessor for setting the zPosition
    override var zPosition: CGFloat {
        get {
            return super.zPosition
        }
        set {
            // do nothing
        }
    }

    /// specialized accessor for setting the zPosition
    var stickyZPosition: CGFloat {
        get {
            return super.zPosition
        }
        set {
            super.zPosition = newValue
        }
    }
}
Enteron answered 25/1, 2018 at 23:23 Comment(2)
This solution works very well on iOS 11!! :) It breaks on iOS 10 but you can add conditional code. Thanks @James H, this was looking "impossible" to fix!Gooey
I'm experiencing the opposite, It breaks on iOS 11.4 but works pretty well on iOS 10. Do you have any suggestion?Sienese
W
11

Try to setup annotation view layer's zPosition (annotationView.layer.zPosition) in:

- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views;
Wurtz answered 3/5, 2013 at 10:40 Comment(1)
This does not works anymore, at least on iOS 11, like @James H explains on his answer below which really works very well.Gooey
A
6

In the delegate function, you can select the pin to force it on top:

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?` {
    ...
    if my annotation is the special one {
        annotationView.isSelected = true
    }
    ...
}
Amabil answered 18/2, 2019 at 1:32 Comment(2)
Nice idea, but it opens the callout, which i dont likeThermometry
I solved this by first setting canShowCallout = false The order is relevant hereThermometry
M
5

Swift 3:

I get pin locations from API and I was having similar issues, the pins that had to be on top weren't. I was able to solve it like this.

var count = 0 // just so we don't get the same index in bottom pins
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {  
    for view in views {
        view.layer.zPosition = CGFloat(count)
    }
    count += 1
    if count > 500 {
        count = 250 // just so we don't end up with 999999999999+ as a value for count, plus I have at least 30 pins that show at the same time and need to have lower Z-Index values than the top pins. 
    }

}

Hope this helps

Mungovan answered 5/12, 2016 at 21:6 Comment(0)
K
2

I'm finding that this reordering the annotation views causes the callout that pops up when one is clicked to no longer be on top of all the annotations. I've even tried refining it so that instead of bringSubviewToFront and sendSubviewToBack, I use insertSubview:aboveSubview and insertSubview:belowSubview: where the second argument is the first annotationView in the list. This would seem to cause much less front to back scattering, but the call outs still pop up under some annotations.

Krispin answered 5/11, 2009 at 14:21 Comment(1)
The fix for that is to always put the selected annotation on top. And yeah dealing with the z order of annotations is a huge PITA.Trioxide
P
0

I really needed to do this, and none of the (current) answers seemed to provide a reliable implementation. They sort of worked, but panning the map, selecting annotations, or zooming in could mess up the order again.

The final, well behaved solution wasn't so trivial, so I'll just outline the steps I took here. The annotation ordering that MKMapView uses doesn't respect the added order, or even the order of an overriden annotations property. So...


Steps

• Create a CADisplayLink
• Every frame, reorder annotations using both the layer zPosition, and the view's ordering in the superview's subviews array.
• If the view is selected, promote it to the front in your ordering scheme
• Tapping on annotations still respects internal MKMapView ordering, despite the already made changes. To counter this, add an MKMapViewDelegate
• In the delegate object's mapView:didSelect: method, check if the selected annotation is what you'd like it to be
• You can figure out the correct/prioritised annotation by running hit tests on the annotations yourself, with your own ordering taken into account
• If the selected annotation is correct, great. If not, manually select the correct annotation using selectAnnotation:animated:


And there you have it. The above method seems to work well, and the performance hit from running this each frame isn't too bad. You could also look at switching to MapBox, which I believe supports annotation ordering, but this isn't always an option for various reasons.

Piccoloist answered 15/9, 2017 at 1:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.