Search code examples
ioscocoamkmapviewtapcallout

Detect tap on title of callout


how do I detect a tap on the title of a callout of a annotation? I already have a right callout accessory and a left one, but I want to detect if a user taps on the title (which is in the center on the callout).

If this is not possible, how do I disable hiding the callout if I tap on the title?


Solution

  • it's little late to the answer your question but I'm dealing with same kind of problem recently and make my solution by my self with trial and error. maybe I can help someone who dealing with same problem.

    you can also use some custom annotation and callout view classes, there are many example out there. but, there is a another simple way to solve this problem without use the complex foreign classes.

    problem definition: I want to be able to trigger some method just by touching in callout view. I don't want to use right or left accesory buttons. but when I touched callout view, view will disapear immediately.

    short explanation of solution: simply we use custom "MKPinAnnotationView" with "hittest" to detect touches in callout view.

    you can also find github project link at the end of the post.

    touchableCallOutsViewController.h

    //
    //  touchableCallOutsViewController.h
    //  touchableCallOuts
    //
    //  Created by yasin turkoglu on 20.11.2012.
    //  Copyright (c) 2012 yasin turkoglu. All rights reserved.
    //
    
    #import <UIKit/UIKit.h>
    #import <MapKit/MapKit.h>
    
    
    @class myCustomPinAnnotationClass;
    
    @interface touchableCallOutsViewController : UIViewController <MKMapViewDelegate> {
        MKMapView *myMapView;
        int pinCounter;
        myCustomPinAnnotationClass *myAnnotation;
        CLLocationCoordinate2D selectedPinCoordinate;
        int selectedPinNumber;
    }
    
    @property (strong, nonatomic) MKMapView *myMapView;
    @property (strong, nonatomic) myCustomPinAnnotationClass *myAnnotation;
    @end
    

    touchableCallOutsViewController.m

    //
    //  touchableCallOutsViewController.m
    //  touchableCallOuts
    //
    //  Created by yasin turkoglu on 20.11.2012.
    //  Copyright (c) 2012 yasin turkoglu. All rights reserved.
    //
    
    #import "touchableCallOutsViewController.h"
    #import "myCustomPinAnnotationClass.h"
    
    @interface touchableCallOutsViewController ()
    
    @end
    
    
    
    @implementation touchableCallOutsViewController
    
    @synthesize myMapView;
    @synthesize myAnnotation;
    
    - (void)viewDidLoad
    {
    
        //first we create mapview and add pin annotation
    
        myMapView = [[MKMapView alloc]initWithFrame:self.view.frame];
        myMapView.delegate = self;
        MKCoordinateRegion region = {{0,0},{1.0,1.0}};
        region.center.latitude = 41.036651; //user defined
        region.center.longitude = 28.983870;//user defined
        [myMapView setRegion:region animated:YES];
        [self.view addSubview:myMapView];
    
        //we define long press recognizer for 2 seconds and add our map view to add new pin when you press 2 seconds on map view
        UILongPressGestureRecognizer *lngPrs = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(handleMyLongPress:)];
        lngPrs.minimumPressDuration = 2.0;
        [myMapView addGestureRecognizer:lngPrs];
    
        pinCounter = 1; //this variable hold pin numbers and increment 1 when new pin added.
    
        myAnnotation = [[myCustomPinAnnotationClass alloc]initWithName:[NSString stringWithFormat:@"Pin %i",pinCounter] description:@"touch here to see pin coordinates" pinNum:pinCounter coordinate:region.center];
        [myMapView addAnnotation:myAnnotation];
    
    }
    
    - (void)handleMyLongPress:(UIGestureRecognizer *)gestureRecognizer
    {
        if (gestureRecognizer.state != UIGestureRecognizerStateBegan) {
    
        }else{
            //when we press more than 2 seconds on map view UILongPressGestureRecognizer triggered this method and call our custom MKPinAnnotationView class and add annotation.
            CGPoint touchPoint = [gestureRecognizer locationInView:myMapView];
            CLLocationCoordinate2D touchMapCoordinate = [myMapView convertPoint:touchPoint toCoordinateFromView:myMapView];
            pinCounter++;
            myAnnotation = [[myCustomPinAnnotationClass alloc]initWithName:[NSString stringWithFormat:@"Pin %i",pinCounter] description:@"touch here to see pin coordinates" pinNum:pinCounter coordinate:touchMapCoordinate];
            [myMapView addAnnotation:myAnnotation];
        }
    }
    
    - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
    {
        //when new annotation added by us, new annotation property will set here
        static NSString *identifier = @"myPins";
        if ([annotation isKindOfClass:[myCustomPinAnnotationClass class]]) {
            myCustomPinAnnotationClass *annotationView = (myCustomPinAnnotationClass *) [self.myMapView dequeueReusableAnnotationViewWithIdentifier:identifier];
    
            if (annotationView == nil) {
                annotationView = [[myCustomPinAnnotationClass alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
            } else {
                annotationView.annotation = annotation;
            }
    
            myAnnotation = (myCustomPinAnnotationClass *)annotation;
    
            annotationView.pinColor = MKPinAnnotationColorGreen;       
            annotationView.enabled = YES;
            annotationView.draggable = YES;
            annotationView.canShowCallout = YES;
            [annotationView setSelected:YES animated:YES];
            return annotationView;
    
        }    
        return nil;
    
    }
    
    - (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
    {
    
        //if you select any annotation to show their callout sellected annotation pin number and coordinate setted and call findcallot method    
        myAnnotation = (myCustomPinAnnotationClass *)view.annotation;
        selectedPinCoordinate = myAnnotation.coordinate;
        selectedPinNumber = myAnnotation.pinNum;
        [self findCallOut:myMapView];
    }
    
    
    - (void)findCallOut:(UIView *)node
    {
        //this method dig our mapview with for loop until find callout view class named "UICalloutView"
    
        if([node isKindOfClass:[NSClassFromString(@"UICalloutView") class]]){
    
            //this method trigered together with callout open animation.
    
            //in UICalloutView we have callout bubble image
            //we dig it with for loop to find this image height
            float buubleHeight = 0.0;
            for(UIImageView *bubbleComponents in node.subviews){
                //when we find callout bubble image take height to set our touchable area height
                buubleHeight = bubbleComponents.frame.size.height;
                break;
            }
    
    
    
            //this method triger with callout open animation as I said before
            //ant this bouncing open animation distort actual size of callout view
            //so when this happens we also get transform value of callout view and make proportion to find exact sizes.
            CGFloat nodeTransformRatio = node.transform.a;
            CGFloat calculatedWidth = roundf(node.frame.size.width / nodeTransformRatio);
            CGFloat calculatedHeight = roundf(buubleHeight / nodeTransformRatio);
    
            //now create a new UIView and sized according to callout view.
            UIView *touchableView = [[UIView alloc]initWithFrame:CGRectMake(0.0, 0.0, calculatedWidth, calculatedHeight)];
            touchableView.userInteractionEnabled = YES;
    
            //we add new single tap gesture recognizer to triger desired method when users touching to the callout
            UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doSomething)];
            [touchableView addGestureRecognizer:singleTap];
            //and finaly we add this newly created view in callout view to receive touches.
            [node addSubview:touchableView];
    
        }else{
            //loop self until find a "UICalloutView"
            for(UIView *child in node.subviews){
                [self findCallOut:child];
            }
        }
    }
    
    - (void)doSomething
    {
        //this method call allert view our preseted variables when users touch callout view.
        UIAlertView *alertHolder = [[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:@"You're touched the Pin %i CallOut",selectedPinNumber] message:[NSString stringWithFormat:@"lat : %f\nlong : %f",selectedPinCoordinate.latitude,selectedPinCoordinate.longitude] delegate:self cancelButtonTitle:nil otherButtonTitles:@"ok",nil];
        [alertHolder show];
    
    }
    
    - (void)didReceiveMemoryWarning
    {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    @end
    

    myCustomPinAnnotationClass.h

    //
    //  myCustomPinAnnotationClass.h
    //  touchableCallOuts
    //
    //  Created by yasin turkoglu on 20.11.2012.
    //  Copyright (c) 2012 yasin turkoglu. All rights reserved.
    //
    
    #import <MapKit/MapKit.h>
    
    
    @interface myCustomPinAnnotationClass : MKPinAnnotationView <MKAnnotation> {
        NSString *_name;
        NSString *_description;
        int _pinNum;
        CLLocationCoordinate2D _coordinate;
    }
    
    @property (copy) NSString *name;
    @property (copy) NSString *description;
    @property (nonatomic, readonly) int pinNum;
    @property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
    
    - (id)initWithName:(NSString*)header description:(NSString*)description pinNum:(int)pinNum coordinate:(CLLocationCoordinate2D)coordinate;
    
    @end
    

    myCustomPinAnnotationClass.m

    //
    //  myCustomPinAnnotationClass.m
    //  touchableCallOuts
    //
    //  Created by yasin turkoglu on 20.11.2012.
    //  Copyright (c) 2012 yasin turkoglu. All rights reserved.
    //
    
    #import "myCustomPinAnnotationClass.h"
    
    @implementation myCustomPinAnnotationClass
    
    @synthesize name = _name;
    @synthesize description = _description;
    @synthesize pinNum = _pinNum;
    @synthesize coordinate = _coordinate;
    
    
    - (id)initWithName:(NSString*)header description:(NSString*)description pinNum:(int)pinNum coordinate:(CLLocationCoordinate2D)coordinate
    {
        self = [super init];
        if (self) {
            _name = [header copy];
            _description = [description copy];
            _pinNum = pinNum;
            _coordinate = coordinate;
        }
        return self;
    }
    
    - (void)setCoordinate:(CLLocationCoordinate2D)newCoordinate
    {
        _coordinate = newCoordinate;
    }
    
    - (NSString *)title {
        return _name;
    }
    
    - (NSString *)subtitle {
        return _description;
    }
    
    - (int)tag {
        return _pinNum;
    }
    
    -(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
    {
        //test touched point in map view
        //when hit test return nil callout close immediately by default
        UIView* hitView = [super hitTest:point withEvent:event];
        // if hittest return nil test touch point 
        if (hitView == nil){
            //dig view to find custom touchable view lately added by us
            for(UIView *firstView in self.subviews){
                if([firstView isKindOfClass:[NSClassFromString(@"UICalloutView") class]]){
                    for(UIView *touchableView in firstView.subviews){
                        if([touchableView isKindOfClass:[UIView class]]){ //this is our touchable view class
                            //define touchable area 
                            CGRect touchableArea = CGRectMake(firstView.frame.origin.x, firstView.frame.origin.y, touchableView.frame.size.width, touchableView.frame.size.height);
                            //test touch point if in touchable area
                            if (CGRectContainsPoint(touchableArea, point)){
                                //if touch point is in touchable area return touchable view as a touched view
                                hitView = touchableView;
                            }
                        }
                    }                
                }
            }
        }
        return hitView;
    }
    
    @end
    

    Here you can download complete project listing with source code https://github.com/ytur/touchableCallOutsForIOSMaps