19
votes

I have a map view that adds annotations more or less like this:

- (MKAnnotationView *)mapView:(MKMapView *)mapView
            viewForAnnotation:(id <MKAnnotation>) annotation
{
    MKAnnotationView *annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation
                                                                       reuseIdentifier:@"MKPinAnnotationView"];
    annotationView.canShowCallout = YES;

    UIButton *detailButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
    [detailButton addTarget:self
                     action:@selector(handleButtonAction)
           forControlEvents:UIControlEventTouchUpInside];
    annotationView.rightCalloutAccessoryView = detailButton;

    return annotationView;
}

In iOS 7, this puts an “i” icon on the right-hand side of the callout. Tapping on the icon triggers mapView:annotationView:calloutAccessoryControlTapped: (on the delegate) and handleButtonAction: (on self). I recently realized, though, that you can also tap anywhere else on the callout and the same two methods are fired.

This happens with a button of type UIButtonTypeDetailDisclosure but it doesn’t seem to happen with a UIButtonTypeCustom button. The delegate method is also not fired when I tap on the callout when there’s no accessory view at all. (That behavior isn’t surprising, of course; what’s surprising is that if the accessory view is a detail-disclosure button then these two methods are fired regardless of whether you tap on the button itself or just somewhere else in the callout.)

I’d like to get rid of the button in the callout—or at least replace it with a button showing my own image instead of the stock “i” icon—while still allowing the user to tap anywhere on the callout to trigger my action. Is this possible? I don’t see an MKMapViewDelegate method that corresponds to “callout tapped”.

5
You could just add a custom IBAction for your own button, and disregard the default MKMapViewDelegateLefteris
@Lefteris I don’t understand what you mean. I build my UI programmatically, so I don’t have IBActions; but do you mean that I can set my own event handler on a button? That’s what I’m doing in the code sample shown. The problem is that I want to catch all taps on the callout, not just the taps on my button.bdesham

5 Answers

28
votes

Try to set custom image for button without changing UIButtonTypeDetailDisclosure type.

UIButton *detailButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];        
[detailButton setImage:[UIImage imageNamed:@"icon"] forState:UIControlStateNormal];

For iOS7 and above this image will be tinted by default. If you want to keep original icon use the following

[[UIImage imageNamed:@"icon"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]

Or if you want to remove icon at all

[detailButton setImage:[UIImage new] forState:UIControlStateNormal];
9
votes

To tap the callout button after the user has clicked on the Annotation view, add a UITapGestureRecognizer in didSelectAnnotationView. This way you can implement tap on the callout without needing the accessory views.

You can then get the annotation object back from the sender for further action.

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self  action:@selector(calloutTapped:)];
    [view addGestureRecognizer:tapGesture];
}

-(void)calloutTapped:(UITapGestureRecognizer *) sender
{
    NSLog(@"Callout was tapped");

    MKAnnotationView *view = (MKAnnotationView*)sender.view;
    id <MKAnnotation> annotation = [view annotation];
    if ([annotation isKindOfClass:[MKPointAnnotation class]])
    {
        [self performSegueWithIdentifier:@"annotationDetailSegue" sender:annotation];
    }
}
7
votes

Dhanu A's solution in Swift 3:

func mapView(mapView: MKMapView, didSelectAnnotationView view:MKAnnotationView) {
    let tapGesture = UITapGestureRecognizer(target:self,  action:#selector(calloutTapped(sender:)))
    view.addGestureRecognizer(tapGesture)
}

func mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView) {
    view.removeGestureRecognizer(view.gestureRecognizers!.first!)
}

func calloutTapped(sender:UITapGestureRecognizer) {
    let view = sender.view as! MKAnnotationView
    if let annotation = view.annotation as? MKPointAnnotation {
        performSegue(withIdentifier: "annotationDetailSegue", sender: annotation)
    }
}
3
votes

If you don't need tap indication, I'd advise to throw a UITapGestureRecognizer into the callout upon creation and add your handling object (perhaps controller) as a target with appropriate action.

0
votes
- (void)mapView:(MKMapView *)mv annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
    Park *parkAnnotation = (Park *)[view annotation];
    switch ([control tag]) {
        case 0: // left button
        {
            NSURL *url = [NSURL URLWithString:parkAnnotation.link];
            [[UIApplication sharedApplication] openURL:url];
        }
            break;

        case 1: // right button
        {
            // build a maps url. This will launch the Maps app on the hardware, and the apple maps website in the simulator
            CLLocationCoordinate2D coordinate = self.locationManager.location.coordinate;
            NSString *url2 = [NSString stringWithFormat:@"http://maps.apple.com/maps?saddr=%f,%f&daddr=%f,%f",coordinate.latitude,coordinate.longitude,parkAnnotation.location.coordinate.latitude,parkAnnotation.location.coordinate.longitude];

            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url2]];
        }
            break;

        default:
            NSLog(@"Should not be here in calloutAccessoryControlTapped, tag=%ld!",(long)[control tag]);
            break;
    }
}