MapKit (MKMapView): zPosition больше не работает на iOS11

В iOS11 функция zPosition перестала работать для annotationView.layer. Каждый раз, когда изменяется область карты.

введите описание изображения здесь

  • Не повезло с оригинальным решением: layer.zPosition = X;
  • Не повезло с методами bringViewToFront/SendViewToBack

Xcode 8.3/9

ОБНОВЛЕНИЕ (РЕШЕНИЕ спасибо Элиасу Аалто):

При создании 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];

}

В MeLikeSingleton или любом объекте наблюдателя, который у вас есть:

- (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);
           }

       } 
}
  • Это решение использует значение layer.name для отслеживания zOrder. Если у вас много уровней zPosition (местоположение пользователя, кластер, контакт, выноска);)
  • Нет для циклов, только KVO
  • Я использовал объект Singleton, который отслеживает изменения значения слоя. Если у вас есть несколько MKMapViews, используемых в приложении.

КАК ЭТО РАБОТАЕТ ДО IOS11

.. - использовать

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

и установите здесь zPosition.

.. но что (для некоторых из нас, тем не менее, dunny why) больше не работает в iOS11!

Ответ 1

ZPosition действительно работает, просто MKMapView перезаписывает его внутренне на основе некорректного и бесполезного MKFeatureDisplayPriority. Если вам просто нужно несколько аннотаций, чтобы упорствовать над "всем остальным", вы можете сделать это получисто, используя KVO. Просто добавьте наблюдателя в слой представления аннотации zPosition и перезапишите его, так как MKMapView пытается возиться с ним.

(Извините, мой ObjC)

Добавить наблюдателя:

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

Отменить 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

Ответ 2

Мы можем полностью игнорировать MKMapView попытки изменить слой MKAnnotationView zPosition. Так как MKAnnotationView использует стандартный CALayer как свой слой, а не какой-то частный класс, мы можем подклассифицировать его и переопределить его zPosition. Чтобы действительно установить zPosition, мы можем предоставить наш собственный аксессор.

Он будет работать намного быстрее, чем 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:

У меня есть один очень неэлегантный метод для выбора самого верхнего аннотационного представления при нажатии на перекрывающиеся аннотации.

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)
    }
}