32
votes

Here is my code. I want to add my own custom callout view instead of iOS default. I know there is only left callout and right callout view, but I need to add a view tooltip type with my own background and label.

    - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
    {
        MKAnnotationView *userAnnotationView = nil;
        if ([annotation isKindOfClass:MKUserLocation.class])
        {
            userAnnotationView = (MKAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:@"UserLocation"];
            if (userAnnotationView == nil)  {
            userAnnotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"UserLocation"];
            }
            else
                userAnnotationView.annotation = annotation;

            userAnnotationView.enabled = YES;


            userAnnotationView.canShowCallout = YES;
            userAnnotationView.image = [UIImage imageNamed:@"map_pin.png"];
            UIView *view = [[UIView alloc]initWithFrame:CGRectMake(0,0,141,108)];
            view.backgroundColor = [UIColor clearColor];
            UIImageView *imgView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"toltip.png"]];
            [view addSubview:imgView];
            userAnnotationView.leftCalloutAccessoryView = view;



            return userAnnotationView;
        }

}

Image for reference

6

6 Answers

133
votes

I had same problem and ended up doing following thing:

When I receive mapView delegate callback

-(void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view

(it's time when I want to show my custom CalloutView)

I use view received as parameter *(MKAnnotationView )view (which is a pin view) and simply add my custom view to that pin view using

 [view addSubview:customView];

It will add your custom view on top of that pin view so if we want it to be above pin we have to change custom view center property like this:

CGRect customViewRect = customView.frame;
        CGRect rect = CGRectMake(-customViewRect.size.width/2, -customViewRect.size.height-7, customViewRect.size.width, customViewRect.size.height);
        customView.frame = rect;
[view addSubview:customView];

In my case it looks like this

enter image description here

!NOTE:

There is one caveat which however can be easily fixed, your custom view will ignore touch events, because of mapView working with touches differently. Here's a quick fix:

1) Subclass MKAnnotationView (or MKPinAnnotationView depends on what you need)

2) in your mapView delegate

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation

use your subclass instead of MKAnnotationView/MKPinAnnotationView

3) in your subclass .m file override two methods as following:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* hitView = [super hitTest:point withEvent:event];
    if (hitView != nil)
    {
        [self.superview bringSubviewToFront:self];
    }
    return hitView;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = self.bounds;
    BOOL isInside = CGRectContainsPoint(rect, point);
    if(!isInside)
    {
        for (UIView *view in self.subviews)
        {
            isInside = CGRectContainsPoint(view.frame, point);
            if(isInside)
                break;
        }
    }
    return isInside;
}

All done!

10
votes

I have created a library to show custom callouts.

DXCustomCallout-ObjC

You can pass any custom view for the callout! It supports events for UIControls as well.

Custom Callout

2
votes

To the best answer above in Swift

This can save some minutes to rewrite itself.

!NOTE:

There is one caveat which however can be easily fixed, your custom view will ignore touch events, because of mapView working with touches differently. Here's a quick fix:

1) Subclass MKAnnotationView (or MKPinAnnotationView depends on what you need)

2) in your mapView delegate

func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?

use your subclass instead of MKAnnotationView/MKPinAnnotationView

3) in your subclass .m file override two methods as following:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {

    let hitView = super.hitTest(point, withEvent: event)

    if (hitView != nil)
    {
        self.superview?.bringSubviewToFront(self)
    }

    return hitView;

}

override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {

    let rect = self.bounds
    var isInside = CGRectContainsPoint(rect, point)

    if(!isInside) {
        for view in self.subviews {

            isInside = CGRectContainsPoint(view.frame, point)
            break;
        }
    }

    return isInside
}
1
votes

I just created a custom callout for my map view which avoids issues with the callout disappearing when tapped. Here is a Gist with the steps I took.

0
votes

You can use this library. It also provides a ready-to-use template for the callout view. Adds cool animations and an anchor view for your custom views.

https://github.com/okhanokbay/MapViewPlus

enter image description here enter image description here