Building Custom Map Annotation Callouts – Part 1

The iPhone’s Map Annotation Callouts are very useful for displaying small amounts of information when a map pin (annotation) is selected. One problem with the standard callouts present in iOS is the inability to change the height of the callout.
For example, you may want to display a logo or other image that is taller than the default callout. Or you may want to display an address and phone number on separate lines under the title. Both of these scenarios are impossible using the standard iOS callouts. There are many steps to building a good replacement callout with the proper look and behavior, but it can be done.
Part 1 (explained here) will explain how to build a custom map callout.Part 2 covers adding a button to the custom callout, which is not as simple as it sounds.

Put it on the map (and take it off)

For this example we will create two simple map annotations in the view controller – one will display the standard callout and the other will display the custom callout.
To place the “custom callout annotation” on the map we will add the custom annotation when the mapView calls the mapView:didSelectAnnotationView: method, and we will remove the callout on the corresponding deselect method, mapView:didDeselectAnnotationView:. In mapView:viewForAnnotation: we return an instance of our custom MKAnnotationView subclass. Also, we disable the standard callout on the “parent” annotation view, which we will show the custom callout for.

Note: If building for iOS 3.x you will need to determine annotation selection another way (KVO, notifications, etc.).

Draw the callout (in the right place)

Now that we have the callout annotation placed on the map at the same coordinate as the parent annotation, we need to adjust the width and height of the callout view and adjust the center offset so that the view spans the entire width of the map and sits above the parent annotation. These calculations will be done during setAnnotation: because our contentHeight, offsetFromParent, and mapView properties should have been set by then. setNeedsDisplay will also be called in setAnnotation: so that the callout is redrawn to match up with the annotation.

The shape of the callout bubble is basically a round-rectangle with a triangle that points to the parent annotation. Determining where that point should be is a matter of finding the x-coordinate of the parent relative to it and adding the offsetFromParent.x property. Luckily UIView contains the handy convertPoint:fromView: method to handle the conversion between coordinate systems.
The steps to draw something similar to the standard callout are as follows:

Create the shape (path) of the callout bubble with the point in the right position to match up with the parent

Fill the path and add the shadow (adding the shadow here and then restoring the context prevents the shadow from being redrawn with each subsequent step)

Apply a stroke to the path (more opaque than the fill)

Create a round rectangle path to appear as the “gloss”

Fill the gloss path with a gradient

Convert the glass path to a “stroked path” (this will allow us to apply a gradient to the stroke)

Let’s Add Some Content

To allow the addition of content we will create a content view as a read-only property, which will allow our consumers to access it. An additional method, prepareContentFrame will be added and invoked from setAnnotation: to set the content frame.

Animation

So far the callout looks similar to the native callout, but it is still lacking some of the behavior of the original. The callout needs to animate out from the parent annotation. Also, when the parent annotation is near the edge of the map view, the map should be adjusted to move the parent annotation in from the edge of the view.
The animation would be fairly simple if we could just adjust the frame of the callout view, however that will not scale the contents of the callout. Thus, we must use a CGAffineTransform. Apple has a good introducton to affine transforms. The transform will need to both scale the view and translate the view to make it appear to grow out of the parent annotation. Scaling is simple – a value of 1 is normal size and other values act as a multiplier, so smaller values shrink the view and larger values expand the view. If the parent is off-center on the x-axis the callout needs to be translated to keep the point fixed directly over the parent annotation. Likewise the y-axis must be translated so that it appears that the callout grows upward from parent. We need to hold on to the frame for these calculations because self.frame cannot be trusted during the animations. The calculations are done in the following two methods:

There will be three steps to the animation to create the bounce-like effect of the standard callout. We cannot begin the animation with a scale of 0 because a transformation matrix with a scale of 0 cannot be inverted.

Grow from very small to slightly larger than the final size

Shrink to slightly smaller than the final size

Grow to the final size

These three steps will be separate animations chained together using UIView’s setAnimationDidStopSelector: and setAnimationDelegate: methods.

Shifting the Map

When the parent annotation is near the edge of the map, the map needs to be shifted so that the parent annotation and the callout remain a certain distance away from the edge of the view. To do this we need to calculate the distance to the edge of the view, the number of degrees latitude and longitude per pixel, and then set the new center point for the map. This adjustment should be made when didMoveToSuperview is called.

About the Author

64 Comments on this Post

First, I’m amazed that no-one else has commented – this is extremely useful code, and I thank you for making it available.
I’ve implemented it in a project that has multiple instances of the custom callout. It works fine in the 4.1 simulator, and works for the most part on a 3.1.3 iPhone.
I appreciate your comment that getting select/deselect events in 3.x requires manual notifications. On the 3.1.3 device, I am not getting the accessory tap events – is that also something that needs a manual notification? Any ideas on how to do that?
Thanks.
-Mike

Ashok

Roy Remington

I love this custom View. One Problem I want to make this much smaller so that it doesn’t cover the entire width of the Map View. Is there an easy way to do this? I tried changing values in your coding only to get some pretty bad results.

The thing I’m looking to do is make it have a yellow background which I managed and add a Name and two buttons, which I have also done. The width is just big for my liking.

Any help on reducing the width would be greatly appreciated! Thanks

Roy Remington

Oh, Never mind. Disregard my previous comment. I managed to figure it out. Had to change the X Origin and then had it automatically zoom into the annotation when it was selected to resolve the problem.

To Roy Remington, That’s exactly the problem I need to solve – could you post some code that shows how you changed the X origin and what you mean by “automatically zoom into the annotation”?
Thanks!
-Mike

Martin

Hi, this code has really put my project forward! Thank you so much.
One problem I’ve encountered is that if I have several annotations, only the first annotations will display a Custom Callout. How do you do that so if the user clicks on a second annotation following the first one, the custom callout will also be displayed?

Great mapping code by the way. The best I’ve seen on the net and in some of the books I have. I encountered a similar problem as Martin on how to have multiple annotations with the CustomCallout. Currently it only works for the first annotation and not the others. Any help would be appreciated. Thanks.

Gavin

Caution – didSelectAnnotationView is only available from iOS 4.0 onwards. Ideally there would be another method for selecting the annotation, probably by handling the touches in the view manually. Still researching…

aArmaan

Hi,
Its really nice code.But I have one problem. I have added button in Calloutmap annotation view but button seems less clickable.Button click method not called. It’s called hardly when I keep pressing upto 2 or 3 seconds. Any help will be appreciated.
Thnaks

Daniel Phillips

This is a good solution, but I can’t help but feel it’s an overkill for me unfortunately, I am only trying to replace the grey callout graphic which is drawn to my custom graphic I’ve made in Photoshop.

I tried this with your code but had no success, I then tried just overriding drawRect of MKAnnotationView for myself, but no luck with the name and subtitle of the annotation.

If you could suggest a way forward, all I really need to do is replace the graphic, maybe reduce the height slightly too… (to match my graphic)

I’m speechless at such an elegant solution. I was freaking out about wether I would be able to handle the code because it’s a little out of my league but it turns out implementation was a piece of cake. Still smoothing out some minor glitches but overall a great piece of code to work with. Thanks!

Shawn

Thanks, this helps a lot! A perplexing bug I have found is if you initially zoom in on the map, then pan the custom pin to the very edge and click on it — it will correctly animate away from the edge but the callout will not be shown. (If you then pan the map some more it will usually pop up).

kevin

This is fantastic, and exactly what I needed for a project I’m working on. The only difference between my project and this example is that CoreLocation updates are constantly changing the coordinates of the pin, so the pin annotation moves around. I can’t figure out how to get the callout annotation to “lock” to the pin, and move around with it onscreen. Any ideas? http://stackoverflow.com/q/6392931/607876

Benjamin

Thanks for this great tutorial. I’m still unclear on how to have the callout’s width match the width of the contentView. Other people have seemed to have solved it, but with only their descriptions to go on I’m lost. Thanks in advance for any help you can offer with this.

Venkat

Jacob Jennings

I created a project based on the drawRect code in this article which takes an interface builder-based approach to custom MKMapView callouts. The view for the callout is loaded from a xib, and the callout resizes based on the supplied view. The project is available here:

Bob

Well this solution worked great for my application. I have multiple custom annotations, all displaying their rightful images/text/etc with callout buttons…and everything works beautifully….in iOS 5.1.

I installed my application on an iPhone running iOS6 and this get very ugly. The map sometimes doesnt adjust when a callout is expanded, so the callout ends up 80% off the screen. Additionally, sometimes the arrow underneath the callout thats supposed to hover over the pin gets drawn at the edge of the screen nowhere near the actual pin (callout). The fact that this code works flawlessly on 5.1 leads me to believe that Apple has some done something on their end that is screwing this up. I’m on a mission though to pinpoint exactly where in the code this issue exists.

Dragos

I’ve been successfully using this solution in my own app for a while now, but in ios 6 with the new maps app it is broken, the triangle is no longer displaying properly on first map view display and as well some time after, the view ends up behind the pins and it also changes size and text ends up outside it. Do you have any idea what the issue is? I’ve been debugging and the weird thing is that the rect size changes if you click from pin to pin without clicking on map first.

Adamthulla K

The above customized annotation view is working fine in iOS versions prior to iOS 6. But in iOS 6 actually the annotation view is not getting displayed properly as the connector arrow is getting misplaced when we clicked first time and after that it is working fine. This issue is only with iOS 6.

Any help regarding the same is very much helpful to me.

Thanks in Advance.

FS

In the prepareFrameSize method, where you have: frame.size = CGSizeMake(self.mapView.frame.size.width, height); you just replace self.mapView.frame.size.width with whatever width you want. Then, you go to the prepareOffset method and make the same replacement on the xOffset declaration. Hope this helps!

Brian L

I have managed to fix the iOS 6 bug where the triangle pointing to the pin is off set with the default set up.
in the drawRect method, after setting the CGFloat parentX add:
if (parentX self.frame.size.width – 70)
parentX = self.frame.size.width – 70;

lakshmi reddy

Hi, thanks for providing good tutorial its helping a lot, i am facing one issue here in ios 6 the triangle in annotation is not properly placed at first time , can u please help me. its very urgent to me

james.rantanen

I had a chance to look into the iOS 6 issue. There are two changes required for proper positioning. I plan to make these changes and transition the project to ARC and will then post updated code.

In CustomMapAnnotationExampleViewController.m set the selected annotation view before adding the annotation to the map view.
self.selectedAnnotationView = view;
[self.mapView addAnnotation:self.calloutAnnotation];

In CalloutMapAnnotationView.m you need to store the xPixelShift that was calculated in -adjustMapRegionIfNeeded and use it in -drawRect: to adjust the parentX value
float osVersion = [[[UIDevice currentDevice] systemVersion] floatValue];
if (osVersion >= 6.0) {
parentX += self.xPixelShift;
}

MuthuKumar

Mehdi

When I first clock on an annotation the first time, subsequent selections create an extra offset. Deselecting the annotation by clicking on the background returns it back to the initial state. But again subsequent selections have an offset. I have implemented the suggestion on the misplaced triangle by James Rantanen comments. Any suggestions please?

4 Trackbacks

[…] Part 1 showed how to build a custom map callout that provides more content flexibility than the native callout, but maintains the expected look and behavior. In part 2 we will add a very common element of the map interface into our custom callout – the accessory button. At first glance this seems simple: just add a button to the callout. However, MapKit intercepts touch events and causes undesired callout behavior. The code used to add an accessory button is also applicable to any other button(s) or responders you may want to add to a callout, giving you the flexibility to do what you feel is best for your users. […]