89

I'm currently working with the mapkit and am stuck.

I have a custom annotation view I am using, and I want to use the image property to display the point on the map with my own icon. I have this working fine. But what I would also like to do is to override the default callout view (the bubble that shows up with the title/subtitle when the annotation icon is touched). I want to be able to control the callout itself: the mapkit only provides access to the left and right ancillary callout views, but no way to provide a custom view for the callout bubble, or to give it zero size, or anything else.

My idea was to override selectAnnotation/deselectAnnotation in my MKMapViewDelegate, and then draw my own custom view by making a call to my custom annotation view. This works, but only when canShowCallout is set to YES in my custom annotation view class. These methods are NOT called if I have this set to NO (which is what I want, so that the default callout bubble is not drawn). So I have no way of knowing if the user touched on my point on the map (selected it) or touched a point that is not part of my annotation views (delected it) without having the default callout bubble view show up.

I tried going down a different path and just handling all touch events myself in the map, and I can't seem to get this working. I read other posts related to catching touch events in the map view, but they aren't exactly what I want. Is there a way to dig into the map view to remove the callout bubble before drawing? I'm at a loss.

Any suggestions? Am I missing something obvious?

Vadim Kotov's user avatar

Vadim Kotov

8,2848 gold badges51 silver badges63 bronze badges

asked Oct 14, 2009 at 12:02

Zach's user avatar

2

55

There is an even easier solution.

Create a custom UIView (for your callout).

Then create a subclass of MKAnnotationView and override setSelected as follows:

- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
    [super setSelected:selected animated:animated];

    if(selected)
    {
        //Add your custom view to self...
    }
    else
    {
        //Remove your custom view...
    }
}

Boom, job done.

Venk's user avatar

Venk

5,9559 gold badges43 silver badges54 bronze badges

answered Aug 5, 2011 at 11:16

TappCandy's user avatar

5 Comments

hi TappCandy! First, thanks for your solution. It does work, but when a load a view (I'm doing it from a nib file) the buttons don't work. What can I do to make them working properly?? Thanks

Love the simplicity of this solution!

Hmm - this doesn't work! It replaces the map annotation (i.e. the pin) NOT the callout "bubble".

This won't work. The MKAnnotationView doesn't have a reference to the mapview so you have several issues with adding a custom callout. For example you don't know if adding this as a subview will be offscreen.

@PapillonUK You have control over the frame of the callout. It is not replacing the pin, it is appearing on top of it. Adjust its position when you add it as a subview.

41

detailCalloutAccessoryView

In the olden days this was a pain, but Apple has solved it, just check the docs on MKAnnotationView

view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
view.canShowCallout = true
view.detailCalloutAccessoryView = UIImageView(image: UIImage(named: "zebra"))

Really, that's it. Takes any UIView.

answered Jul 23, 2012 at 12:44

Cameron Lowell Palmer's user avatar

1 Comment

Which one is best to use?

13

Continuing on from @TappCandy's brilliantly simple answer, if you want to animate your bubble in the same way as the default one, I've produced this animation method:

- (void)animateIn
{   
    float myBubbleWidth = 247;
    float myBubbleHeight = 59;

    calloutView.frame = CGRectMake(-myBubbleWidth*0.005+8, -myBubbleHeight*0.01-2, myBubbleWidth*0.01, myBubbleHeight*0.01);
    [self addSubview:calloutView];

    [UIView animateWithDuration:0.12 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^(void) {
        calloutView.frame = CGRectMake(-myBubbleWidth*0.55+8, -myBubbleHeight*1.1-2, myBubbleWidth*1.1, myBubbleHeight*1.1);
    } completion:^(BOOL finished) {
        [UIView animateWithDuration:0.1 animations:^(void) {
            calloutView.frame = CGRectMake(-myBubbleWidth*0.475+8, -myBubbleHeight*0.95-2, myBubbleWidth*0.95, myBubbleHeight*0.95);
        } completion:^(BOOL finished) {
            [UIView animateWithDuration:0.075 animations:^(void) {
                calloutView.frame = CGRectMake(-round(myBubbleWidth/2-8), -myBubbleHeight-2, myBubbleWidth, myBubbleHeight);
            }];
        }];
    }];
}

It looks fairly complicated, but as long as the point of your callout bubble is designed to be centre-bottom, you should just be able to replace myBubbleWidth and myBubbleHeight with your own size for it to work. And remember to make sure your subviews have their autoResizeMask property set to 63 (i.e. "all") so that they scale correctly in the animation.

:-Joe

answered Oct 24, 2011 at 12:41

jowie's user avatar

jowie

8,0588 gold badges61 silver badges95 bronze badges

9 Comments

Btw, you could animation on the scale property rather than the frame, in order to get proper scaling of the content.

Don't use CGRectMake. Use CGTransform.

@occulus - I think I tried that first, but had rendering issues in iOS 4. But that might have been because I wasn't using CABasicAnimation... Too long ago to remember! I know I'd have to animate the y property as well, due to the center offset.

@CameronLowellPalmer - why not use CGRectMake?

@jowie Because the syntax is better, and avoids the magic values in your code. CGAffineTransformMakeScale(1.1f, 1.1f); To grow the box by 110% is clear. The way you went about it might work, but it isn't pretty.

|

7

Found this to be the best solution for me. You'll have to use some creativity to do your own customizations

In your MKAnnotationView subclass, you can use

- (void)didAddSubview:(UIView *)subview{
    int image = 0;
    int labelcount = 0;
    if ([[[subview class] description] isEqualToString:@"UICalloutView"]) {
        for (UIView *subsubView in subview.subviews) {
            if ([subsubView class] == [UIImageView class]) {
                UIImageView *imageView = ((UIImageView *)subsubView);
                switch (image) {
                    case 0:
                        [imageView setImage:[UIImage imageNamed:@"map_left"]];
                        break;
                    case 1:
                        [imageView setImage:[UIImage imageNamed:@"map_right"]];
                        break;
                    case 3:
                        [imageView setImage:[UIImage imageNamed:@"map_arrow"]];
                        break;
                    default:
                        [imageView setImage:[UIImage imageNamed:@"map_mid"]];
                        break;
                }
                image++;
            }else if ([subsubView class] == [UILabel class]) {
                UILabel *labelView = ((UILabel *)subsubView);
                switch (labelcount) {
                    case 0:
                        labelView.textColor = [UIColor blackColor];
                        break;
                    case 1:
                        labelView.textColor = [UIColor lightGrayColor];
                        break;

                    default:
                        break;
                }
                labelView.shadowOffset = CGSizeMake(0, 0);
                [labelView sizeToFit];
                labelcount++;
            }
        }
    }
}

And if the subview is a UICalloutView, then you can screw around with it, and what's inside it.

Kevin Reid's user avatar

Kevin Reid

46.3k14 gold badges102 silver badges128 bronze badges

answered Aug 31, 2011 at 20:36

яοвοτағτєяаււ's user avatar

4 Comments

how do you check if the subview is a UICalloutView since UICalloutview is not a public class.

if ([[[subview class] description] isEqualToString:@"UICalloutView"]) I think there is a better way, but this works.

did you have any issues with this since as Manish pointed out, it's a private class.

Probably they changed the UIView structure for the calloutviews. I haven't upgraded the app that uses this, so you're on your own :P

6

I had the same problem. There is a serious of blog posts about this topic on this blog http://spitzkoff.com/craig/?p=81.

Just using the MKMapViewDelegate doesn't help you here and subclassing MKMapView and trying to extend the existing functionality also didn't work for me.

What I ended up doing is to create my own CustomCalloutView that I am having on top of my MKMapView. You can style this view in any way you want.

My CustomCalloutView has a method similar to this one:


- (void) openForAnnotation: (id)anAnnotation
{
    self.annotation = anAnnotation;
    // remove from view
    [self removeFromSuperview];

    titleLabel.text = self.annotation.title;

    [self updateSubviews];
    [self updateSpeechBubble];

    [self.mapView addSubview: self];
}

It takes an MKAnnotation object and sets its own title, afterward it calls two other methods which are quite ugly which adjust the width and size of the callout contents and afterward draw the speech bubble around it at the correct position.

Finally the view is added as a subview to the mapView. The problem with this solution is that it is hard to keep the callout at the correct position when the map view is scrolled. I am just hiding the callout in the map views delegate method on a region change to solve this problem.

It took some time to solve all those problems, but now the callout almost behaves like the official one, but I have it in my own style.

Venk's user avatar

Venk

5,9559 gold badges43 silver badges54 bronze badges

answered Oct 16, 2009 at 10:35

Sascha Konietzke's user avatar

3 Comments

I second that: UICalloutView is a private class and not available in the SDK officially. Your best bet is to follow Sascha's advice.

Hi Sascha, We thank you for your reply. But, the current problem that we are experiencing is how to get the position (x,y and not lat or long) on the map where the pins appear, so that we can display the callout view.

converting coordinates can easily be done by two function of MKMapView: # – convertCoordinate:toPointToView: # – convertPoint:toCoordinateFromView:

5

Basically to solve this, one needs to: a) Prevent the default callout bubble from coming up. b) Figure out which annotation was clicked.

I was able to achieve these by: a) setting canShowCallout to NO b) subclassing, MKPinAnnotationView and overriding the touchesBegan and touchesEnd methods.

Note: You need to handle the touch events for the MKAnnotationView and not MKMapView

Comments

3

I just come up with an approach, the idea here is

  // Detect the touch point of the AnnotationView ( i mean the red or green pin )
  // Based on that draw a UIView and add it to subview.
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
{
    CGPoint newPoint = [self.mapView convertCoordinate:selectedCoordinate toPointToView:self.view];
//    NSLog(@"regionWillChangeAnimated newPoint %f,%f",newPoint.x,newPoint.y);
    [testview  setCenter:CGPointMake(newPoint.x+5,newPoint.y-((testview.frame.size.height/2)+35))];
    [testview setHidden:YES];
}

- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
    CGPoint newPoint = [self.mapView convertCoordinate:selectedCoordinate toPointToView:self.view];
//    NSLog(@"regionDidChangeAnimated newPoint %f,%f",newPoint.x,newPoint.y);
    [testview  setCenter:CGPointMake(newPoint.x,newPoint.y-((testview.frame.size.height/2)+35))];
    [testview setHidden:NO];
}

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view 
{  
    NSLog(@"Select");
    showCallout = YES;
    CGPoint point = [self.mapView convertPoint:view.frame.origin fromView:view.superview];
    [testview setHidden:NO];
    [testview  setCenter:CGPointMake(point.x+5,point.y-(testview.frame.size.height/2))];
    selectedCoordinate = view.annotation.coordinate;
    [self animateIn];
}

- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view 
{
    NSLog(@"deSelect");
    if(!showCallout)
    {
        [testview setHidden:YES];
    }
}

Here - testview is a UIView of size 320x100 - showCallout is BOOL - [self animateIn]; is the function that does view animation like UIAlertView.

Venk's user avatar

Venk

5,9559 gold badges43 silver badges54 bronze badges

answered Jan 26, 2012 at 11:08

Nikesh K's user avatar

Nikesh K

6216 silver badges10 bronze badges

2 Comments

What does selectedCoordinate specify? What type of Object is that?

it is CLLocationCoordinate2d object.

1

You can use leftCalloutView, setting annotation.text to @" "

Please find below the example code:

pinView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:defaultPinID];
if(pinView == nil){
    pinView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:defaultPinID] autorelease];       
}
CGSize sizeText = [annotation.title sizeWithFont:[UIFont fontWithName:@"HelveticaNeue" size:12] constrainedToSize:CGSizeMake(150, CGRectGetHeight(pinView.frame))                                 lineBreakMode:UILineBreakModeTailTruncation];
pinView.canShowCallout = YES;    
UILabel *lblTitolo = [[UILabel alloc] initWithFrame:CGRectMake(2,2,150,sizeText.height)];
lblTitolo.text = [NSString stringWithString:ann.title];
lblTitolo.font = [UIFont fontWithName:@"HelveticaNeue" size:12];
lblTitolo.lineBreakMode = UILineBreakModeTailTruncation;
lblTitolo.numberOfLines = 0;
pinView.leftCalloutAccessoryView = lblTitolo;
[lblTitolo release];
annotation.title = @" ";            

Suresh Varma's user avatar

Suresh Varma

9,7402 gold badges63 silver badges91 bronze badges

answered Apr 14, 2011 at 9:18

Fabio Napodano's user avatar

1 Comment

This will 'work' to some extent, but doesn't really answer the more general question asked and it is very hackish.

1

I've pushed out my fork of the excellent SMCalloutView that solves the issue with providing a custom view for callouts and allowing flexible widths/heights pretty painlessly. Still some quirks to work out, but it's pretty functional so far:

https://github.com/u10int/calloutview

answered Oct 17, 2012 at 23:04

u10int's user avatar

u10int

2192 silver badges8 bronze badges

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.