Anirudha Mahale
Anirudha Mahale

Reputation: 2596

Clustering doesn't work properly in iOS 13

I am doing clustering of Annotations.

The below code works fine and clusters the points correctly in iOS 11 & iOS 12.

This fails to cluster the points decluster the points in iOS 13.

I am not using any beta versions.

The TTMapView class is wrapper for MKMapView.

class TTMapView: UIView {

    var mapView = MKMapView()
    private var mapObjects: Dictionary<TTShape, MKShape?> = [:]
    private var _isClusteringEnabled = true

    func addMarker(_ marker: TTPoint) -> TTPoint {
        removeMarker(marker)
        let coordinate = marker.coordinate
        let pointAnnotation = MKPointAnnotation()
        pointAnnotation.coordinate = convertTTCoordinateToCLLocationCoordinate2D(coordinate)
        pointAnnotation.title = marker.title
        pointAnnotation.subtitle = marker.subtitle
        mapObjects.updateValue(pointAnnotation, forKey: marker)
        mapView.addAnnotation(pointAnnotation)
        return marker
    }
}

extension TTMapView: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        if annotation is MKUserLocation {
            return nil
        }

        if _isClusteringEnabled {
            let point = mapObjects.filter ({ $0.value === annotation }).first?.key as? TTPoint
            print("point ", point)
            return TTClusterAnnotationView(annotation: annotation, reuseIdentifier: TTClusterAnnotationView.ReuseID, image: point?.image, color: point?.tintColor)
        } else {
            let reuseId = "simplePin"
            var pinAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
            if pinAnnotationView == nil {
                pinAnnotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
                pinAnnotationView?.isDraggable = true
                pinAnnotationView?.canShowCallout = true
            }
            return pinAnnotationView
        }
    }
}

class TTClusterAnnotationView: MKMarkerAnnotationView {

    /// Use this Id for setting annotation
    static let ReuseID = "clusterAnnotation"

    init(annotation: MKAnnotation?, reuseIdentifier: String?, image: UIImage?, color: UIColor? = nil) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        // Enable clustering by just setting the clusteringIdentifier
        clusteringIdentifier = "clusteringIdentifier"
        glyphImage = image
        glyphTintColor = color
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func prepareForDisplay() {
        super.prepareForDisplay()
        displayPriority = .required
    }
}

Upvotes: 0

Views: 1198

Answers (1)

Rob
Rob

Reputation: 437542

Make the cluster annotation view (this is the annotation view for the cluster, not to be confused with your existing annotation view that has a clusteringIdentifier, which I’ve renamed to CustomAnnotationView to avoid confusion) have a displayPriority of .required:

class CustomClusterAnnotationView: MKMarkerAnnotationView {
    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        displayPriority = .required
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var annotation: MKAnnotation? {
        didSet {
            displayPriority = .required
        }
    }
}

Then register that class:

mapView.register(CustomClusterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)

And then use it:

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    if annotation is MKUserLocation {
        return nil
    }

    if annotation is MKClusterAnnotation {
        return nil
    }

    ...
}

A few unrelated observations:

  1. I’d suggest just adding your image and color properties to a custom annotation type rather than having viewFor filter through mapObjects. So:

    class CustomAnnotation: MKPointAnnotation {
        let image: UIImage
        let color: UIColor
    
        init(coordinate: CLLocationCoordinate2D, title: String?, image: UIImage, color: UIColor) {
            self.image = image
            self.color = color
            super.init()
            self.coordinate = coordinate
            self.title = title
        }
    }
    

    Then, if you use that rather than MKPointAnnotation, your custom annotation view can pull the color and image info right out of the annotation.

    class CustomAnnotationView: MKMarkerAnnotationView {
        static let reuseID = "CustomAnnotationView"
    
        override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
            super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
            updateForAnnotation()
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override var annotation: MKAnnotation? {
            didSet {
                updateForAnnotation()
            }
        }
    }
    
    private extension CustomAnnotationView {
        func updateForAnnotation() {
            clusteringIdentifier = "CustomAnnotationView"
            displayPriority = .required
            if let annotation = annotation as? CustomAnnotation {
                glyphImage = annotation.image
                glyphTintColor = annotation.color
            } else {
                glyphImage = nil
                glyphTintColor = nil
            }
        }
    }
    

    Note, above I’m resetting the cluster identifier, image, glyph, etc. in the didSet of annotation. This enables the reuse of annotation views. (See next point.)

  2. The reuse logic for your pin annotation view is not correct. And you’re not doing reuse at all if clustering is turned on. If targeting iOS 11 and later, I’d use dequeueReusableAnnotationView(withIdentifier:for:) which takes care of all of this for you. So, I can register this reuse id:

    mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: CustomAnnotationView.reuseID)
    

    And I’d repeat that process for the “simple pin” annotation view that you show if you turn off clustering.

    mapView.register(CustomSimplePinAnnotationView.self, forAnnotationViewWithReuseIdentifier: CustomSimplePinAnnotationView.reuseID)
    

    And

    class CustomSimplePinAnnotationView: MKPinAnnotationView {
        static let reuseID = "CustomSimplePinAnnotationView"
    
        override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
            super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
            isDraggable = true
            canShowCallout = true
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    

    And then your viewFor is simplified:

    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        switch annotation {
        case is MKUserLocation:
            return nil
    
        case is MKClusterAnnotation:
            return nil
    
        default:
            if _isClusteringEnabled {
                return mapView.dequeueReusableAnnotationView(withIdentifier: CustomAnnotationView.reuseID, for: annotation)
            } else {
                return mapView.dequeueReusableAnnotationView(withIdentifier: CustomSimplePinAnnotationView.reuseID, for: annotation)
            }
        }
    }
    

Upvotes: 3

Related Questions