Bug? `MKAnnotationView` not properly removed from map view. Or: How to use `collisionMode`?

Summary: First of all, I feel fairly confident that there is a bug related to removing annotations from a map that are displayed using MKAnnotationView. Secondly, this leads me to the question of how to use the collisionMode property in order to auto-hide overlapping annotation views. Surely, this property is not exclusively made for MKMarkerAnnotationView?!

I have created a screen recording video that visualises the issue, but I won't post the link here. Please feel free to ask.

Here’s my problem: I want to have a map that can switch between two states: either display a certain set of annotations as regular balloon shaped MKMarkerAnnotationView markers on a map, or display a different set of annotations as MKAnnotationView (either a custom subclass with some drawing or the default class with an image property; it doesn't make a difference).

So far so good, but one observation so far, that I don't understand: MKMarkerAnnotationView have the ability to auto-hide themselves when they get too close to another annotation view that has its collisionMode property not set to .none. How is this achieved? Does the MKMapView do this or is a MKMarkerAnnotationView aware of its surroundings (I don’t think so)? I can't find a way to create the same effect for any other kind of annotation view (MKAnnotationView), they would always overlap.

The bug: When my app is in said state where it displays annotations as MKAnnotationView (e.g. an image) and I switch back to display MKMarkerAnnotationView the map appears to retain ”ghost“ collision frames (or something) on the map of all MKAnnotationViews that are supposed to be removed. Those invisible collision areas will remain statically on the map view even when the user moves the map region around. This means, wherever there had been an MKAnnotationView on the screen just before the related annotations were removed, the now displayed MKMarkerAnnotationView markers will auto-hide when they come close to those areas even if there is nothing in the way.

To be clear, I call mapView.removeAnnotations(annotations) to remove all annotations and I double-checked in debugging that no more annotations are on the mapView, yet somewhere, somehow internally the mapView seems to retain the collision areas/frames in absolute point coordinates relative to the view on screen (not to earth coordinates) as they don't move with further map movement.

Is there anything else I have to do after calling mapView.removeAnnotations to properly clear the map view?

This happens in the simulator and on device with iOS 14.2, iOS 13, but it does not happen in the simulator with iOS 12.

Additionally, I can confirm that when I set the collisionMode property of the MKAnnotationViews to .none (supported in iOS 14 only), this issue does not occur (pretty much as expected).

The whole auto-hiding and collision stuff is a bit of a mystery to me and I could find little about it in the documentation. Any help would be much appreciated! Thanks!
Annotations are reused.
The default implementation of prepareForReuse does nothing.

So is it possible that when creating annotations, you are including left-over information, from previous instantiations?

(This could create some unexpected (and mysterious!) behaviors.)

I think the first step would be to look at your annotation's implementation of prepareForReuse, and check that you are resetting all the relevant information.

Apple say: "You can override prepareForReuse in your custom annotation views and use it to put the view in a known state before it is returned to your map view delegate."
I have uploaded a full demo app here: https://github.com/Manc/GhostAnnotations

@robnotyou: Thanks for your feedback. I don't think I do anything unusual that could cause an issue. I try to keep things as minimal as possible. At the moment I don't to anything in prepareForReuse and I tried to unset the image, but it doesn't make a difference. It seems prepareForReuse is only called shortly before it is required, so as far as I understand it should not interfere with other annotation views.
The print statement in viewFor annotation seems to confirm that the annotation is not being (fully) reset, before it is reused.
We can still see the old value for "frame".

In prepareForReuse, try adding:
self.frame = CGRect.zero
@robnotyou, thanks for taking a look, but unfortunately it didn't help. From what I can gather by looking at my print statements, the lifecycle of an annotation view goes a bit like this:
  1. A new (in my example) image annotation is being added to the map.

  2. mapView(_:viewFor:) is called, because the map wants a view for that annotation.

  3. I know what kind of view that image annotation wants, so I call mapView.dequeueReusableAnnotationView(withIdentifier:for:).

  4. Because this is the first time this kind of "image annotation view" is requested, a new instance will automatically be created and returned. I return this instance without further modification here.

  5. Method prepareForDisplay of that "image annotation view" is being called, just before being displayed. Here, I set the view's properties, including the concrete UIImage.

  6. Now the user removes all annotations from the map – mapView.removeAnnotations(…).

  7. Nothing significant happens, except for the map removing the views, but apparently not properly clearing the collision frames (or whatever). Note: prepareForDisplay of the image views is not being called at this point, so setting the frame to .zero or anything else is not even possible, unless there is some other method that I don't know of that would be responsible for clearing up the map, but it would not make any sense that collision frames had to be cleared manually by the developer when annotations are being removed from the map.

  8. Now the user adds marker annotations to the map.

  9. mapView(_:viewFor:)

  10. This time we return marker views after calling mapView.dequeueReusableAnnotationView(withIdentifier:for:).

  11. Method prepareForDisplay of the marker views is being called.

  12. Bug: Markers are displayed, but now they would falsely detect a collision at the screen positions where the image annotations were previously.

Only when the user switches back to image annotations, and we call mapView.dequeueReusableAnnotationView(withIdentifier:for:) to get those views, this time prepareForReuse() of the "image annotation views" will be called, so it's too late and resetting the frame to .zero has no effect, since we will indirectly set the frame to the same value again anyway, only in nanoseconds when prepareForDisplay is being called.
I think I found a workaround. I tried my best to solve this properly, but for now I assume that this is a bug in MapKit. I also assume that MKMarkerAnnotationViews get special treatment by MKMapView that enables them to auto-hide and do other things related to collisions (including clustering etc.), align marker titles etc.

My workaround (and it's only just that, not a clean solution) goes like this:

I use mapView(_ mapView:didAdd views:) to keep track of annotation views added to the map. Then, just before I call mapView.removeAnnotations(…), I iterate over all annotation views and set isHidden = true. This seems to be enough to remove collision frames from the map.
Bug? `MKAnnotationView` not properly removed from map view. Or: How to use `collisionMode`?
 
 
Q