How to customize the callout bubble for MKAnnotationView?
Asked Answered
E

9

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?

Embay answered 14/10, 2009 at 12:2 Comment(2)
This link doesn't work, but found the same post here -> Building Custom Map Annotation Callouts – Part 1Bathhouse
You can refer to this project for a quick demo. github.com/akshay1188/CustomAnnotation Compilation of answers above into one running demo.Tancred
L
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.

Largehearted answered 5/8, 2011 at 11:16 Comment(5)
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?? ThanksAbalone
Love the simplicity of this solution!Dogtooth
Hmm - this doesn't work! It replaces the map annotation (i.e. the pin) NOT the callout "bubble".Eclair
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.Regulus
@Eclair 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.Lewse
R
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.

Regulus answered 23/7, 2012 at 12:44 Comment(1)
Which one is best to use?Russ
I
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

Iotacism answered 24/10, 2011 at 12:41 Comment(9)
Btw, you could animation on the scale property rather than the frame, in order to get proper scaling of the content.Friar
Don't use CGRectMake. Use CGTransform.Regulus
@Friar - 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.Iotacism
@CameronLowellPalmer - why not use CGRectMake?Iotacism
@Iotacism 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.Regulus
@CameronLowellPalmer - unfortunately that didn't work correctly for me under iOS 4. I guess if you are using Core Graphics calls, you need to animate them using CABasicAnimation.Iotacism
@Iotacism Every UIView has a transform. So try this in iOS 4, although I must say supporting iOS 4 at this point is not really necessary. Check my post below. I will paste the Animation block.Regulus
@CameronLowellPalmer thanks for this, though it was a long time ago I wrote this code now and it works for me so I'll just keep it as it is so I don't introduce any more bugs :) however, if you're not supporting iOS 4, can't you just use UIAppearance?Iotacism
@Iotacism I'm familiar with UIAppearance, but I'm not sure how it would apply here.Regulus
P
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.

Pension answered 31/8, 2011 at 20:36 Comment(4)
how do you check if the subview is a UICalloutView since UICalloutview is not a public class.Soraya
if ([[[subview class] description] isEqualToString:@"UICalloutView"]) I think there is a better way, but this works.Litterbug
did you have any issues with this since as Manish pointed out, it's a private class.Chrissychrist
Probably they changed the UIView structure for the calloutviews. I haven't upgraded the app that uses this, so you're on your own :PLitterbug
S
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.

Sanction answered 16/10, 2009 at 10:35 Comment(3)
I second that: UICalloutView is a private class and not available in the SDK officially. Your best bet is to follow Sascha's advice.Transmissible
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.Embay
converting coordinates can easily be done by two function of MKMapView: # – convertCoordinate:toPointToView: # – convertPoint:toCoordinateFromView:Sanction
E
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

Eliott answered 14/10, 2009 at 12:2 Comment(0)
H
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.

Hauser answered 26/1, 2012 at 11:8 Comment(2)
What does selectedCoordinate specify? What type of Object is that?Ransell
it is CLLocationCoordinate2d object.Lookout
A
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 = @" ";            
Assamese answered 14/4, 2011 at 9:18 Comment(1)
This will 'work' to some extent, but doesn't really answer the more general question asked and it is very hackish.Regulus
I
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

Impersonate answered 17/10, 2012 at 23:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.