Mkmapview Not Clustering Annotation on Zooming Out Map in Swift

MKMapView not Clustering Annotation on zooming out map in Swift

OK, the iOS 11 and later solution is fairly simple. You have two annotation views, one for your own annotations, and one for clusters of annotations. Your main annotation view simply has to specify the clusteringIdentifier when it’s initialized and when the annotation property changes:

class UserAnnotationView: MKMarkerAnnotationView {
static let preferredClusteringIdentifier = Bundle.main.bundleIdentifier! + ".UserAnnotationView"

override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
clusteringIdentifier = UserAnnotationView.preferredClusteringIdentifier
collisionMode = .circle
}

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

override var annotation: MKAnnotation? {
willSet {
clusteringIdentifier = UserAnnotationView.preferredClusteringIdentifier
}
}
}

And your cluster annotation view should just update its image when its annotation property is updated:

class UserClusterAnnotationView: MKAnnotationView {
static let preferredClusteringIdentifier = Bundle.main.bundleIdentifier! + ".UserClusterAnnotationView"

override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
collisionMode = .circle
updateImage()
}

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

override var annotation: MKAnnotation? { didSet { updateImage() } }

private func updateImage() {
if let clusterAnnotation = annotation as? MKClusterAnnotation {
self.image = image(count: clusterAnnotation.memberAnnotations.count)
} else {
self.image = image(count: 1)
}
}

func image(count: Int) -> UIImage {
let bounds = CGRect(origin: .zero, size: CGSize(width: 40, height: 40))

let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { _ in
// Fill full circle with tricycle color
AppTheme.blueColor.setFill()
UIBezierPath(ovalIn: bounds).fill()

// Fill inner circle with white color
UIColor.white.setFill()
UIBezierPath(ovalIn: bounds.insetBy(dx: 8, dy: 8)).fill()

// Finally draw count text vertically and horizontally centered
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.black,
.font: UIFont.boldSystemFont(ofSize: 20)
]

let text = "\(count)"
let size = text.size(withAttributes: attributes)
let origin = CGPoint(x: bounds.midX - size.width / 2, y: bounds.midY - size.height / 2)
let rect = CGRect(origin: origin, size: size)
text.draw(in: rect, withAttributes: attributes)
}
}
}

Then, all you have to do is register your classes:

mapView.register(UserAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
mapView.register(UserClusterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)

No mapView(_:viewFor:) implementation is needed (nor desired). But the above yields (showing the default animation as you zoom out and back in):

Sample Image

Now, clearly, you can modify your UserAnnotationView however you want. (Your question didn’t indicate what the standard, single user annotation view would look like). But by setting its clusteringIdentifier and registering a MKMapViewDefaultClusterAnnotationViewReuseIdentifier you get clustering fairly easily in iOS 11 and later.

If you really want to make the cluster annotation views look like the standard annotation views, you can register the same annotation view class for both:

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

But you then have to give the cluster annotation view the same clusteringIdentifier that we previously gave to the standard annotation view:

class UserClusterAnnotationView: MKAnnotationView {
static let preferredClusteringIdentifier = Bundle.main.bundleIdentifier! + ".UserClusterAnnotationView"

override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
clusteringIdentifier = UserClusterAnnotationView.preferredClusteringIdentifier
collisionMode = .circle
updateImage()
}

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

override var annotation: MKAnnotation? {
didSet {
clusteringIdentifier = UserClusterAnnotationView.preferredClusteringIdentifier
updateImage()
}
}

private func updateImage() {
if let clusterAnnotation = annotation as? MKClusterAnnotation {
self.image = image(count: clusterAnnotation.memberAnnotations.count)
} else {
self.image = image(count: 1)
}
}

func image(count: Int) -> UIImage {
let bounds = CGRect(origin: .zero, size: CGSize(width: 40, height: 40))

let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { _ in
// Fill full circle with tricycle color
AppTheme.blueColor.setFill()
UIBezierPath(ovalIn: bounds).fill()

// Fill inner circle with white color
UIColor.white.setFill()
UIBezierPath(ovalIn: bounds.insetBy(dx: 8, dy: 8)).fill()

// Finally draw count text vertically and horizontally centered
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.black,
.font: UIFont.boldSystemFont(ofSize: 20)
]

let text = "\(count)"
let size = text.size(withAttributes: attributes)
let origin = CGPoint(x: bounds.midX - size.width / 2, y: bounds.midY - size.height / 2)
let rect = CGRect(origin: origin, size: size)
text.draw(in: rect, withAttributes: attributes)
}
}
}

That yields:

Sample Image

Personally, I think that’s a little confusing, but if that’s what you’re going for, that’s one way to achieve it.


Now, if you really need to support iOS versions prior to 11 and you want clustering, then you’ll have to do all this clustering logic yourself (or find third party library to do it). Apple shows how to do this in WWDC 2011 Visualizing Information Geographically with MapKit. 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.

This is non-trivial, so I’d think long and hard about whether I wanted to implement this myself. I’d either abandon iOS versions prior to 11 or find a third-party implementation (and that question you reference has plenty of examples).

Annotations are hidden when zooming out

The current user's annotation doesn't have a cluster identifier. If you provide a cluster identifier for the current user's annotation, it works.

Custom clustered annotations on MapKit crash when rapidly dragging the map or changing the zoom-level

The cause for your crash is that you don't account for other annotations requested by map kit (e.g. MKUserLocation). You are triggering this due to the automatic clustering as you set clusteringIdentifier to a non-nil value.

Just return nil when you want to deal with the annotation yourself so MKMapView uses the default handling:

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

let view = mapView.dequeueReusableAnnotationView(withIdentifier: "identifier", for: annotation)
view.clusteringIdentifier = "clusterIdentifer"
// …

return view
}

If you ever want to customize the cluster annotations just add a special case for MKClusterAnnotation. And if you show user location don't forget to return nil for MKUserLocation if you want the default blue dot.

Zooming MKMapView to fit annotation pins?

You've got it right.

Find your maximum and minimum latitudes and longitudes, apply some simple arithmetic, and use MKCoordinateRegionMake.

For iOS 7 and above, use showAnnotations:animated:, from MKMapView.h:

// Position the map such that the provided array of annotations are all visible to the fullest extent possible. 
- (void)showAnnotations:(NSArray *)annotations animated:(BOOL)animated NS_AVAILABLE(10_9, 7_0);

Mkmap iOS11 clusters doesn't split up after max zoom, how to set it up?

You will need to keep track of the zoom level of the map, and reload your annotations when you cross a zoom level that you specify.

private let maxZoomLevel = 9
private var previousZoomLevel: Int?
private var currentZoomLevel: Int? {
willSet {
self.previousZoomLevel = self.currentZoomLevel
}
didSet {
// if we have crossed the max zoom level, request a refresh
// so that all annotations are redrawn with clustering enabled/disabled
guard let currentZoomLevel = self.currentZoomLevel else { return }
guard let previousZoomLevel = self.previousZoomLevel else { return }
var refreshRequired = false
if currentZoomLevel > self.maxZoomLevel && previousZoomLevel <= self.maxZoomLevel {
refreshRequired = true
}
if currentZoomLevel <= self.maxZoomLevel && previousZoomLevel > self.maxZoomLevel {
refreshRequired = true
}
if refreshRequired {
// remove the annotations and re-add them, eg
let annotations = self.mapView.annotations
self.mapView.removeAnnotations(annotations)
self.mapView.addAnnotations(annotations)
}
}
}

private var shouldCluster: Bool {
if let zoomLevel = self.currentZoomLevel, zoomLevel <= maxZoomLevel {
return false
}
return true
}

func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// https://stackoverflow.com/a/40616239/883413
let zoomWidth = mapView.visibleMapRect.size.width
let zoomLevel = Int(log2(zoomWidth))
self.currentZoomLevel = zoomLevel
}

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// for me, annotation reuse doesn't work with clustering
let annotationView = CustomAnnotationView(annotation: annotation)
if self.shouldCluster {
annotationView.clusteringIdentifier = "custom-id"
} else {
annotationView.clusteringIdentifier = nil
}
return annotationView
}

Zoom MKMapView to an MKAnnotationView's frame

Start by removing the group pin

[mapView removeAnnotation:groupAnnotation];

Then add the pins in the cluster

[mapView addAnnotations:clusterAnnotations];

Then determine the region to zoom to

CLLocationDegrees minLat = 90;
CLLocationDegrees maxLat = -90;
CLLocationDegress minLong = 180;
CLLocationDegrees maxLong = -180
[clusterAnnotations enumerateUsingBlock:^(id<MKAnnotation> annotation, NSUInteger idx, BOOL *stop) {
CLLocationCoordinate2D coordinate = annotation.coordinate;
minLat = MIN(minLat, coordinate.latitude);
maxLat = MAX(maxLat, coordinate.latitude);
minLong = MIN(minLong, coordinate.longitude);
maxLong = MAX(maxLong, coordinate.longitude);
}
CLLocationCoordinate2D center = CLLocationCoordinate2DMake((minLat + maxLat)/2.f, (minLong + maxLong)/2.f);
MKCoordinateSpan span = MKCoordinateSpanMake((maxLat - minLat)*1.25, (maxLong - minLong)*1.25); //1.25 is for padding
MKCoordinateRegion region = MKCoordinateRegionMake(center, span);
[mapView setRegion:[mapView regionThatFits:region] animated:YES];


Related Topics



Leave a reply



Submit