I am trying to make cluster views for annotations that are really close to each other on my Apple Map. I know Apple has native cluster view kit came out with iOS 11, but all tutorials I could find online are written in Swift. I hope somebody could teach me or recommand me any tutorials that i could read to find out how to implement clustered annotations in Objective-C.
My idea is to create a ClusterView class, which inherits the MKAnnotationView class, and then create a instance of the ClusterView in the mapView controller.
I have read the documentation from apple, it only provides you with functions that I might need to call, but it didn't explain how to use them, this is the link to Apple Documentation:https://developer.apple.com/documentation/mapkit/mkclusterannotation?language=objc
Any help would be appreciated!
Here are the basic steps:
Define your annotation view, specifying clusteringIdentifier
and collisionMode
:
// CustomAnnotationView.h
@import MapKit;
@interface CustomAnnotationView : MKMarkerAnnotationView
@end
and
// CustomAnnotationView.m
#import "CustomAnnotationView.h"
static NSString *identifier = @"com.domain.clusteringIdentifier";
@implementation CustomAnnotationView
- (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier {
if ((self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier])) {
self.clusteringIdentifier = identifier;
self.collisionMode = MKAnnotationViewCollisionModeCircle;
}
return self;
}
- (void)setAnnotation:(id<MKAnnotation>)annotation {
[super setAnnotation:annotation];
self.clusteringIdentifier = identifier;
}
@end
Optionally, if you want, you can define your own cluster annotation view, specifying displayPriority
and collisionMode
. This one also updates the image for the cluster to indicate how many annotations are clustered:
// ClusterAnnotationView.h
@import MapKit;
@interface ClusterAnnotationView : MKAnnotationView
@end
and
// ClusterAnnotationView.m
#import "ClusterAnnotationView.h"
@implementation ClusterAnnotationView
- (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier {
if ((self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier])) {
self.displayPriority = MKFeatureDisplayPriorityDefaultHigh;
self.collisionMode = MKAnnotationViewCollisionModeCircle;
}
return self;
}
- (void)setAnnotation:(id<MKAnnotation>)annotation {
super.annotation = annotation;
[self updateImage:annotation];
}
- (void)updateImage:(MKClusterAnnotation *)cluster {
if (!cluster) {
self.image = nil;
return;
}
CGRect rect = CGRectMake(0, 0, 40, 40);
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:rect.size];
self.image = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
// circle
[[UIColor blueColor] setFill];
[[UIColor whiteColor] setStroke];
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:rect];
path.lineWidth = 0.5;
[path fill];
[path stroke];
// count
NSString *text = [NSString stringWithFormat:@"%ld", (long) cluster.memberAnnotations.count];
NSDictionary<NSAttributedStringKey, id> *attributes = @{
NSFontAttributeName: [UIFont preferredFontForTextStyle: UIFontTextStyleBody],
NSForegroundColorAttributeName: [UIColor whiteColor]
};
CGSize size = [text sizeWithAttributes:attributes];
CGRect textRect = CGRectMake(rect.origin.x + (rect.size.width - size.width) / 2,
rect.origin.y + (rect.size.height - size.height) / 2,
size.width,
size.height);
[text drawInRect:textRect withAttributes:attributes];
}];
}
@end
You don’t have to create your own subclass for the cluster if you don’t want to. But this just illustrates how you can completely control the appearance of the cluster, should you choose to do so.
Then your view controller just needs to register the appropriate classes and you’re done (no map view delegate needed):
[self.mapView registerClass:[CustomAnnotationView class] forAnnotationViewWithReuseIdentifier:MKMapViewDefaultAnnotationViewReuseIdentifier];
If you want to use your custom clustering view, you can register that, too:
[self.mapView registerClass:[ClusterAnnotationView class] forAnnotationViewWithReuseIdentifier:MKMapViewDefaultClusterAnnotationViewReuseIdentifier];
For example:
// ViewController.m
#import “ViewController.h"
@import MapKit;
#import "CustomAnnotationView.h"
#import "ClusterAnnotationView.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self configureMapView];
}
- (void)configureMapView {
self.mapView.userTrackingMode = MKUserTrackingModeFollow;
[self.mapView registerClass:[CustomAnnotationView class] forAnnotationViewWithReuseIdentifier:MKMapViewDefaultAnnotationViewReuseIdentifier];
[self.mapView registerClass:[ClusterAnnotationView class] forAnnotationViewWithReuseIdentifier:MKMapViewDefaultClusterAnnotationViewReuseIdentifier];
}
// I’m going to search for restaurants and add annotations for those,
// but do whatever you want
- (void)performSearch {
MKLocalSearchRequest *request = [[MKLocalSearchRequest alloc] init];
request.naturalLanguageQuery = @"restaurant";
request.region = self.mapView.region;
MKLocalSearch *search = [[MKLocalSearch alloc] initWithRequest:request];
[search startWithCompletionHandler:^(MKLocalSearchResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"%@", error);
return;
}
for (MKMapItem *mapItem in response.mapItems) {
MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
annotation.coordinate = mapItem.placemark.coordinate;
annotation.title = mapItem.name;
annotation.subtitle = mapItem.placemark.thoroughfare;
[self.mapView addAnnotation:annotation];
}
}];
}
@end
That yields: