I've seen this in some apps so far that when the user zooms out then the annotations move closer to each other and if they're too close then they're replaced by e.g. a '+5' pin or something else.
How to do that?
I think it should be done in regionDidChangeAnimated
and checking the distance on mapview (not the real distance) for each Pin to each other.
Would that be the right approach? How to get the distance on mapview instead of distance inside it (e.g. distance between NY and SF will always be the same, but if user zooms out, the distance between the pins on the map shrink)
In WWDC 2017 What's New in MapKit they introduce us to a new annotation clustering API in iOS 11 that makes it incredibly easy to achieve clustering. In short, set a clusteringIdentifier
for your annotation view, and it takes care of all of the clustering logic for you. E.g.
override func viewDidLoad() {
super.viewDidLoad()
mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
mapView.register(ClusterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)
...
}
And, in with this, no MKMapViewDelegate
methods are needed (though you can obviously do so if you want further customization, but the default reuse identifiers eliminate the need for that if you're OK with the basic UX. The key is, that you must implement your annotation views, notably setting clusteringIdentifier
for your main annotation view, so clustering will happen automatically, e.g.
class CustomAnnotationView: MKMarkerAnnotationView {
static let clusteringIdentifier = "ClusterAnnotationView"
let annotationWidth = 40
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
clusteringIdentifier = CustomAnnotationView.clusteringIdentifier
collisionMode = .circle
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var annotation: MKAnnotation? {
willSet {
clusteringIdentifier = CustomAnnotationView.clusteringIdentifier
// you can do whatever other update to your `newValue` annotation needed here, if you'd like
}
}
}
And
class ClusterAnnotationView: MKAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
displayPriority = .defaultHigh
collisionMode = .circle
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var annotation: MKAnnotation? {
willSet {
updateImage(for: newValue as? MKClusterAnnotation)
}
}
private func updateImage(for cluster: MKClusterAnnotation?) {
guard let cluster = cluster else { image = nil; return }
let rect = CGRect(origin: .zero, size: CGSize(width: 40, height: 40))
let renderer = UIGraphicsImageRenderer(size: rect.size)
image = renderer.image { _ in
// e.g. circle
#colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1).setFill()
#colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0).setStroke()
let path = UIBezierPath(arcCenter: rect.center, radius: rect.radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
path.lineWidth = 0.5
path.fill()
path.stroke()
// with count in the center
let text = "\(cluster.memberAnnotations.count)"
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.font: UIFont.boldSystemFont(ofSize: 20)]
let size = text.size(withAttributes: attributes)
let textRect = CGRect(origin: CGPoint(x: rect.midX - size.width / 2, y: rect.midY - size.height / 2), size: size)
text.draw(in: textRect, withAttributes: attributes)
}
}
}
All of the complicated manual clustering contemplated in my original answer, below, is no longer needed.
In WWDC 2011 Visualizing Information Geographically with MapKit, they illustrate a way to do precisely that (the guts of the demo start roughly 18 minutes into the video). The concept they employ is the notion of dividing the visible map into a grid, and if there are multiple annotations within a particular grid, they remove them and add a single "cluster" annotation. And they illustrate how you might even visually animate the moving of the annotations in and out of the cluster, so the user can understand what's going on as they zoom in and out. It's a nice starting point as you dive into this.