There are several approaches to customizing callouts:
The easiest approach is to use the existing right and left callout accessories, and put your button in one of those. For example:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation {
static NSString *identifier = @"MyAnnotationView";
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}
MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (view) {
view.annotation = annotation;
} else {
view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
view.canShowCallout = true;
view.animatesDrop = true;
view.rightCalloutAccessoryView = [self yesButton];
}
return view;
}
- (UIButton *)yesButton {
UIImage *image = [self yesButtonImage];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(0, 0, image.size.width, image.size.height); // don't use auto layout
[button setImage:image forState:UIControlStateNormal];
[button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventPrimaryActionTriggered];
return button;
}
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control {
NSLog(@"%s", __PRETTY_FUNCTION__);
}
If you really don't like the button on the right, where accessories generally go, you can turn off that accessory, and iOS 9 offers the opportunity to specify thedetailCalloutAccessoryView, which replaces the callout's subtitle with whatever view you want:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation {
static NSString *identifier = @"MyAnnotationView";
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}
MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (view) {
view.annotation = annotation;
} else {
view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
view.canShowCallout = true;
view.animatesDrop = true;
}
view.detailCalloutAccessoryView = [self detailViewForAnnotation:annotation];
return view;
}
- (UIView *)detailViewForAnnotation:(PlacemarkAnnotation *)annotation {
UIView *view = [[UIView alloc] init];
view.translatesAutoresizingMaskIntoConstraints = false;
UILabel *label = [[UILabel alloc] init];
label.text = annotation.placemark.name;
label.font = [UIFont systemFontOfSize:20];
label.translatesAutoresizingMaskIntoConstraints = false;
label.numberOfLines = 0;
[view addSubview:label];
UIButton *button = [self yesButton];
[view addSubview:button];
NSDictionary *views = NSDictionaryOfVariableBindings(label, button);
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[label]|" options:0 metrics:nil views:views]];
[view addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label]-[button]|" options:0 metrics:nil views:views]];
return view;
}
- (UIButton *)yesButton {
UIImage *image = [self yesButtonImage];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.translatesAutoresizingMaskIntoConstraints = false; // use auto layout in this case
[button setImage:image forState:UIControlStateNormal];
[button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventPrimaryActionTriggered];
return button;
}
If you really want to develop a custom callout yourself, theLocation and Maps Programming Guideoutlines the steps involved:
In an iOS app, it’s good practice to use themapView:annotationView:calloutAccessoryControlTapped:delegate method to respond when users tap a callout view’s control (as long as the control is a descendant ofUIControl). In your implementation of this method you can discover the identity of the callout view’s annotation view so that you know which annotation the user tapped. In a Mac app, the callout view’s view controller can implement an action method that responds when a user clicks the control in a callout view.
When you use a custom view instead of a standard callout, you need to do extra work to make sure your callout shows and hides appropriately when users interact with it. The steps below outline the process for creating a custom callout that contains a button:
Design anNSVieworUIViewsubclass that represents the custom callout. It’s likely that the subclass needs to implement thedrawRect:method to draw your custom content.
Create a view controller that initializes the callout view and performs the action related to the button.
In the annotation view, implementhitTest:to respond to hits that are outside the annotation view’s bounds but inside the callout view’s bounds, as shown in Listing 6-7.
In the annotation view, implementsetSelected:animated:to add your callout view as a subview of the annotation view when the user clicks or taps it.
If the callout view is already visible when the user selects it, thesetSelected:method should remove the callout subview from the annotation view (see Listing 6-8).
In the annotation view’sinitWithAnnotation:method, set thecanShowCalloutproperty toNOto prevent the map from displaying the standard callout when the user selects the annotation.
That previous point outlines a pretty complicated scenarios (i.e. you have to write your own code to detecting taps outside the view in order to dismiss the it). If you're supporting iOS 9, you might just use a popover view controller, e.g.:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation {
static NSString *identifier = @"MyAnnotationView";
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}
MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (view) {
view.annotation = annotation;
} else {
view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
view.canShowCallout = false; // note, we're not going to use the system callout
view.animatesDrop = true;
}
return view;
}
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
PopoverController *controller = [self.storyboard instantiateViewControllerWithIdentifier:@"AnnotationPopover"];
controller.modalPresentationStyle = UIModalPresentationPopover;
controller.popoverPresentationController.sourceView = view;
// adjust sourceRect so it's centered over the annotation
CGRect sourceRect = CGRectZero;
sourceRect.origin.x += [mapView convertCoordinate:view.annotation.coordinate toPointToView:mapView].x - view.frame.origin.x;
sourceRect.size.height = view.frame.size.height;
controller.popoverPresentationController.sourceRect = sourceRect;
controller.annotation = view.annotation;
[self presentViewController:controller animated:TRUE completion:nil];
[mapView deselectAnnotation:view.annotation animated:true]; // deselect the annotation so that when we dismiss the popover, the annotation won't still be selected
}
Another approach that avoids having to get into the weeds of the third approach, above, is to add the callouts, themselves, as annotations.
So you'd have two types of annotations, your standard annotation, and these callout annotations. This would appear to be the approach adopted by tochi, and I'd agree that this is a more robust way to tackle the issue. Thus, the idea would be that when you select one of your standard annotation views, you would add this new callout annotation. For example:
// when user selects standard annotation view, add the callout annotation and select it
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
if([view.annotation isKindOfClass:[CustomAnnotation class]]) {
CalloutAnnotation *calloutAnnotation = [[CalloutAnnotation alloc] initForAnnotation:view.annotation];
[mapView addAnnotation:calloutAnnotation];
dispatch_async(dispatch_get_main_queue(), ^{
[mapView selectAnnotation:calloutAnnotation animated:YES];
});
}
}
// when user deselects callout annotation view (i.e. taps anywhere other than the callout annotation), remove the callout annotation
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view
{
if([view.annotation isKindOfClass:[CalloutAnnotation class]]) {
[mapView removeAnnotation:view.annotation];
}
}
TheviewForAnnotationfor the standard annotation is unremarkable, performing your existing code for the standard annotation (just make sure to turn off the system generated callout withcanShowCallout), but if the annotation is aCalloutAnnotation, you'd then create aMKAnnotationView, such as something like this over-simplified example:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation
{
static NSString *customIdentifier = @"CustomAnnotation";
static NSString *calloutIdentifier = @"CalloutAnnotation";
if ([annotation isKindOfClass:[CustomAnnotation class]]) {
MKPinAnnotationView *view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:customIdentifier];
view.canShowCallout = NO; // make sure to turn off standard callout
return view;
} else if ([annotation isKindOfClass:[CalloutAnnotation class]]) {
CGSize size = CGSizeMake(100.0, 80.0);
MKAnnotationView *view = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:calloutIdentifier];
view.frame = CGRectMake(0.0, 0.0, size.width, size.height);
view.backgroundColor = [UIColor whiteColor];
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame = CGRectMake(5.0, 5.0, size.width - 10.0, size.height - 10.0);
[button setTitle:@"OK" forState:UIControlStateNormal];
[button addTarget:self action:@selector(didTouchUpInsideCalloutButton:) forControlEvents:UIControlEventTouchUpInside];
[view addSubview:button];
view.canShowCallout = NO;
view.centerOffset = CGPointMake(0.0, -kMyCalloutOffset);
return view;
}
return nil;
}