MKAnnotation callout with a custom XIB
Objective-C Shell
Switch branches/tags
Nothing to show
Pull request Compare This branch is 6 commits behind j4n0:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.

Annotations with custom callouts

Based on code by Asynchrony Solutions and several authors.

XIB based annotation


We can't create a XIB backed callout, but we can customize the annotation view. So the trick is to add a second annotation when the first is selected, and make the 2nd annotation view look like a callout bubble. This results in a completely customizable virtual callout.

Class diagram

What we see in this image is:

  • AnnotationProtocol: protocol to implement by annotations who wish to handle view creation themselves.
  • AnnotationViewProtocol: protocol to implement by annotations who wish to handle view selection themselves.
  • Annotation & AnnotationView: the real annotation.
  • CalloutAnnotation & CalloutAnnotationView: the fake callout bubble.
  • BaseCalloutView: MKAnnotationView.
  • Content: This class is passed along with the final data to create the callout.

To add an annotation to the map:

CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(0.,0.);
Annotation *ann = [[Annotation alloc]initWithCoordinate:coord]
[self.mapView addAnnotation:ann];

After adding an annotation, the MKMapViewDelegate is invoked, which in turn delegates the view creation to the annotation itself.

-[MKMapViewDelegate mapView:viewForAnnotation:]
  -[Annotation annotationViewInMap] // view creation is delegated to the annotation
    -[AnnotationView initWithAnnotation:reuseIdentifier:] // the view is initialized with the annotation

Now we have a map with a Annotation and AnnotationView on it. Touching the annotation view calls MKMapViewDelegate, which in turn delegates the selection to the view. The view creates a CalloutAnnotation and adds it to the map.

-[MKMapViewDelegate mapView:didSelectAnnotationView:]
  -[AnnotationView didSelectAnnotationViewInMap:]
    -[CalloutAnnotation initWithLat:lon:]

Now we have the same thing, the view creation is delegated to CalloutAnnotation, which creates a CalloutAnnotationView, which uses Core Graphics to create a callout bubble.

CalloutView has a CalloutView.XIB. The code to paint the bubble around the XIB is in the superclass BaseCalloutView.




Annotation        : NSObject         <MKAnnotation, AnnotationProtocol>
AnnotationView    : MKAnnotationView <AnnotationViewProtocol>
BaseCalloutView   : MKAnnotationView <AnnotationViewProtocol>
CalloutAnnotation : NSObject         <MKAnnotation, AnnotationProtocol>
CalloutView       : BaseCalloutView 


Sequence diagram

Annotation        --(added)creates-->    AnnotationView      (the pin)
AnnotationView    --(selected)creates--> CalloutAnnotation   (the annotation acting as delegate to create the bubble)
CalloutAnnotation --(added)creates-->    CalloutView         (the bubble)
CalloutView       --paints-->            <fake callout>      (the bubble painting code)


To create an annotation:

Content *content = [Content new];
content.iconURL = [[NSBundle mainBundle] URLForResource:@"emoji-ghost" withExtension:@"png"];
content.calloutView = [MyCalloutView class];
content.coordinate = coord;
content.values = [NSDictionary dictionaryWithObjectsAndKeys:@"Booo!",@"title", nil];
Annotation *anno = [[Annotation alloc] initWithContent:content];

Then on MyCalloutView:

#import "CalloutView.h"
#import "CalloutAnnotation.h"
@interface MyCalloutView : CalloutView
@property (nonatomic, retain) IBOutlet UILabel* title;
-(IBAction) handleTouch:(id)sender;
- (id) initWithAnnotation:(CalloutAnnotation*)annotation;

#import "MyCalloutView.h"
@implementation MyCalloutView
@synthesize title = _title;
-(IBAction) handleTouch:(id)sender {
    debug(@"touch %@", sender);
- (id)initWithAnnotation:(CalloutAnnotation*)annotation {
    self = [super initWithAnnotation:annotation];
    self.title.text = [annotation.content.values objectForKey:@"title"];
    return self;
- (void)dealloc {
    self.title = nil;
    [super dealloc];