objective-c - tutorial - mkpointannotation color




MapKit(MKMapView): zPosition does not work anymore on iOS11 (2)

On iOS11 the zPosition stopped working for the annotationView.layer. Every time the map region changes.

  • No luck with original solution: layer.zPosition = X;
  • No luck with bringViewToFront/SendViewToBack methods

Xcode 8.3/9

UPDATE (SOLUTION thanks Elias Aalto):

When creating MKAnnotationView:

annotationView.layer.zPosition = 50;
if (IS_OS_11_OR_LATER) {
    annotationView.layer.name = @"50";
    [annotationView.layer addObserver:MeLikeSingleton forKeyPath:@"zPosition" options:0 context:NULL];

}

In MeLikeSingleton or whatever observer object you have there:

- (void)observeValueForKeyPath:(NSString *)keyPath
                         ofObject:(id)object
                           change:(NSDictionary *)change
                          context:(void *)context {

       if (IS_OS_11_OR_LATER) {

           if ([keyPath isEqualToString:@"zPosition"]) {
               CALayer *layer = object;
               int zPosition = FLT_MAX;
               if (layer.name) {
                   zPosition = layer.name.intValue;
               }
               layer.zPosition = zPosition;
               //DDLogInfo(@"Name:%@",layer.name);
           }

       } 
}
  • This solution uses the layer.name value to keep track of zOrder. In case you have many levels of zPosition (user location, cluster, pin, callout) ;)
  • No for loops, only KVO
  • I used a Singleton Object that observs the layer value changes. In case you have multiple MKMapViews used through out the app.

HOW IT WAS WORKING BEFORE IOS11

..is to use the

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

and set the zPosition here.

..but that (for some of us, still dunny why) does not work anymore in iOS11!


The zPosition does work, it's just that MKMapView overwrites it internally based on the somewhat broken and useless MKFeatureDisplayPriority. If you just need a handful of annotations to persist on top of "everything else", you can do this semi cleanly by using KVO. Just add an observer to the annotation view's layer's zPosition and overwrite it as MKMapView tries to fiddle with it.

(Please excuse my ObjC)

Add the observer:

        [self.annotationView.layer addObserver:self forKeyPath:@"zPosition" options:0 context:nil];

Overrule MKMapView

 - (void)observeValueForKeyPath:(NSString *)keyPath
                  ofObject:(id)object
                    change:(NSDictionary *)change
                   context:(void *)context
{
    if(object == self.annotationView.layer)
    {
        self.annotationView.layer.zPosition = FLT_MAX;
    }
}

Profit


We can completely ignore MKMapView's attempts to modify MKAnnotationView layer's zPosition. Since MKAnnotationView uses standard CALayer as its layer and not some private class, we can subclass it and override its zPosition. To actually set zPosition we can provide our own accessor.

It will work much faster than KVO.

class ResistantLayer: CALayer {

    override var zPosition: CGFloat {
        get { return super.zPosition }
        set {}
    }
    var resistantZPosition: CGFloat {
        get { return super.zPosition }
        set { super.zPosition = newValue }
    }
}

class ResistantAnnotationView: MKAnnotationView {

    override class var layerClass: AnyClass {
        return ResistantLayer.self
    }
    var resistantLayer: ResistantLayer {
        return self.layer as! ResistantLayer
    }
}

UPDATE:

I've got one very inelegant method for selection of the topmost annotation view when tapping on overlapping annotations.

class MyMapView: MKMapView {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

        // annotation views whose bounds contains touch point, sorted by visibility order
        let views =
            self.annotations(in: self.visibleMapRect)
                .flatMap { $0 as? MKAnnotation }
                .flatMap { self.view(for: $0) }
                .filter { $0.bounds.contains(self.convert(point, to: $0)) }
                .sorted(by: {
                    view0, view1 in

                    let layer0  = view0.layer
                    let layer1  = view1.layer
                    let z0      = layer0.zPosition
                    let z1      = layer1.zPosition

                    if z0 == z1 {
                        if  let subviews = view0.superview?.subviews,
                            let index0 = subviews.index(where: { $0 === view0 }),
                            let index1 = subviews.index(where: { $0 === view1 })
                        {
                            return index0 > index1
                        } else {
                            return false
                        }
                    } else {
                        return z0 > z1
                    }
                })

        // disable every annotation view except topmost one
        for item in views.enumerated() {
            if item.offset > 0 {
                item.element.isEnabled = false
            }
        }

        // re-enable annotation views after some time
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            for item in views.enumerated() {
                if item.offset > 0 {
                    item.element.isEnabled = true
                }
            }
        }

        // ok, let the map view handle tap
        return super.hitTest(point, with: event)
    }
}