swiftuikitmapkit

Custom Annotation for MapView doesn't always get used


I created a custom annotation using this as an example: custom annotation. It's a white water drop with a blue background. I'm subclassing MKMarkerAnnotationView, rather than MKAnnotationView, to inherit all the normal behaviors of the default marker (ex. it becomes larger when clicked).

The problem is, after zooming and panning, the default red pin occasionally appears instead of my custom blue water marker. After zooming in and out several times, almost all the markers turn into the default red pin. Is this a Swift bug, or am I doing something wrong?

enter image description here

import UIKit
import MapKit

class ViewController: UIViewController {

    let coordinates = [
        CLLocationCoordinate2D(latitude: 37.3105042, longitude: -122.0380024),
        CLLocationCoordinate2D(latitude: 37.3099890, longitude: -122.0404633),
        CLLocationCoordinate2D(latitude: 37.3573748, longitude: -122.0692252),
        CLLocationCoordinate2D(latitude: 37.3285758, longitude: -122.0787571),
        CLLocationCoordinate2D(latitude: 37.3311494, longitude: -122.0588439),
        CLLocationCoordinate2D(latitude: 37.3261889, longitude: -122.0626754),
        CLLocationCoordinate2D(latitude: 37.3578807, longitude: -122.0535947),
        CLLocationCoordinate2D(latitude: 37.3497026, longitude: -122.0562993),
        CLLocationCoordinate2D(latitude: 37.3403821, longitude: -121.9736835),
        CLLocationCoordinate2D(latitude: 37.3766856, longitude: -122.0302783),
        CLLocationCoordinate2D(latitude: 37.3656187, longitude: -122.0378590),
        CLLocationCoordinate2D(latitude: 37.3655657, longitude: -122.0374650),
        CLLocationCoordinate2D(latitude: 37.3242644, longitude: -122.0387466),
        CLLocationCoordinate2D(latitude: 37.3425773, longitude: -122.0247380),
        CLLocationCoordinate2D(latitude: 37.3426291, longitude: -122.0253387),
        CLLocationCoordinate2D(latitude: 37.3416711, longitude: -122.0256557),
        CLLocationCoordinate2D(latitude: 37.3275180, longitude: -122.0507106),
        CLLocationCoordinate2D(latitude: 37.3187770, longitude: -122.0688523),
        CLLocationCoordinate2D(latitude: 37.3412129, longitude: -121.9753722),
        CLLocationCoordinate2D(latitude: 37.3408061, longitude: -121.9757454),
        CLLocationCoordinate2D(latitude: 37.3597423, longitude: -121.9893887),
        CLLocationCoordinate2D(latitude: 37.3242567, longitude: -122.0117839)
    ]

    var mapView: MKMapView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        mapView = MKMapView(frame: self.view.frame)
        view.addSubview(mapView)

        mapView.register(WaterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
        mapView.register(WaterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)

        addAnnotations()

        // center map at first coordinate
        let region = MKCoordinateRegion(center: coordinates[0], span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
        mapView.setRegion(region, animated: true)
    }
    
    private func addAnnotations() {
        for coordinate in coordinates {
            let annotation = MKPointAnnotation()
            annotation.coordinate = coordinate
            mapView.addAnnotation(annotation)
        }
    }
}
class WaterAnnotationView: MKMarkerAnnotationView {
    
    override var annotation: MKAnnotation? {
        didSet { configure(for: annotation) }
    }
    
    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        glyphImage = UIImage(systemName: "drop.fill")
        markerTintColor = #colorLiteral(red: 0.005868499167, green: 0.5166643262, blue: 0.9889912009, alpha: 1)
        configure(for: annotation)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(for annotation: MKAnnotation?) {
        displayPriority = .required
        clusteringIdentifier = MKMapViewDefaultClusterAnnotationViewReuseIdentifier
    }
}

Solution

  • The problem is unrelated to clustering. Temporarily turn off clustering and you will see the same behavior.

    You can move the setting of glyphImage and markerTintColor into the configure(for:) method, to make sure they do not get reset upon reuse.

    class WaterAnnotationView: MKMarkerAnnotationView {
        
        override var annotation: MKAnnotation? {
            didSet { configure(for: annotation) }
        }
        
        override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
            super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    
            // move these lines to `configure(for:)`
            //
            // glyphImage = UIImage(systemName: "drop.fill")
            // markerTintColor = #colorLiteral(red: 0.005868499167, green: 0.5166643262, blue: 0.9889912009, alpha: 1)
    
            configure(for: annotation)
        }
        
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        func configure(for annotation: MKAnnotation?) {
            // move it here
    
            glyphImage = UIImage(systemName: "drop.fill")
            markerTintColor = #colorLiteral(red: 0.005868499167, green: 0.5166643262, blue: 0.9889912009, alpha: 1)
    
            // and then as in your original …
    
            displayPriority = .required
            clusteringIdentifier = MKMapViewDefaultClusterAnnotationViewReuseIdentifier
        }
    }
    

    Alternatively, you could set these in init and reset them back to the desired settings in prepareForReuse, but that seems no better than the above.


    Unrelated, it seems curious to use WaterAnnotationView for your clustering identifier. Specifically, if used for clustering, it seems strange to set the glyphImage that will not be used. I might be inclined to have two different annotation views, one for the “water annotation” and one for the clustering of these annotation views:

    class WaterAnnotationView: MKMarkerAnnotationView {
        override var annotation: MKAnnotation? {
            didSet { configure(for: annotation) }
        }
    
        override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
            super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
            configure(for: annotation)
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        func configure(for annotation: MKAnnotation?) {
            glyphImage = UIImage(systemName: "drop.fill")
            markerTintColor = #colorLiteral(red: 0.005868499167, green: 0.5166643262, blue: 0.9889912009, alpha: 1)
            displayPriority = .required
            clusteringIdentifier = MKMapViewDefaultClusterAnnotationViewReuseIdentifier
        }
    }
    
    class WaterAnnotationClusterView: MKMarkerAnnotationView {
        override var annotation: MKAnnotation? {
            didSet { configure(for: annotation) }
        }
    
        override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
            super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
            configure(for: annotation)
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        func configure(for annotation: MKAnnotation?) {
            displayPriority = .required
            markerTintColor = #colorLiteral(red: 0.005868499167, green: 0.5166643262, blue: 0.9889912009, alpha: 1)
        }
    }
    

    And then, of course:

    mapView.register(WaterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
    mapView.register(WaterAnnotationClusterView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)