How to customize the callout bubble for MKAnnotationView?

IosObjective CMapkitMkmapviewMkannotationview

Ios Problem Overview


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?

Ios Solutions


Solution 1 - Ios

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.

Solution 2 - Ios

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.

Solution 3 - Ios

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

Solution 4 - Ios

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.

Solution 5 - Ios

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];
}

  • (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.

Solution 6 - Ios

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

Solution 7 - Ios

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.

Solution 8 - Ios

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 = @" ";			

Solution 9 - Ios

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionZachView Question on Stackoverflow
Solution 1 - IosTappCandyView Answer on Stackoverflow
Solution 2 - IosCameron Lowell PalmerView Answer on Stackoverflow
Solution 3 - IosjowieView Answer on Stackoverflow
Solution 4 - IosяοвοτағτєяаււView Answer on Stackoverflow
Solution 5 - IosSascha KonietzkeView Answer on Stackoverflow
Solution 6 - IosShivaraj TenginakaiView Answer on Stackoverflow
Solution 7 - IosNikesh KView Answer on Stackoverflow
Solution 8 - IosFabio NapodanoView Answer on Stackoverflow
Solution 9 - Iosu10intView Answer on Stackoverflow