How To Completely Customise Your Map Annotations Callout Views

Update September 2016: Fully updated for Xcode 8 and Swift 3.

In a previous tutorial, you learnt how to customise the map annotations’ callouts. The detailCalloutAccessoryView property was a nice add to the MKAnnotationView class in iOS 9.

However, such extension was limited by only providing a left, right and detail callout views. More than that, you always had to assign a title string to the pin annotation in order to make the callout show up for you. And that actually wasn’t always useful.

So why does a complete annotation callout customisation comes in handy in some circumstances?

Nice question, let’s say you don’t want to put a title in your annotation callout? Well, just leave the title string empty! But that will let to something like this:

Annotation callout

The title has left an annoying white space at the top of the callout view.

Also, in many cases, you may find yourself uncomfortable with the three system-provided accessories views (left, detail and right) and would want to reorganise the callout elements in a way to add more controls to the view or set your own layout. Fortunately, MapKit leaves a room for you to completely customise the map annotations callout views.

And this tutorial is here to guide you step by step on how to do so.

As always, let’s do this!

Begin by downloading the starter project to work with here.

Expand the project files in the Project navigator view, you will notice I prepared a custom xib file called “CustomCalloutView” that contains three labels and a UIImageView, along with a UIView class to manage its outlets.

This UIView will replace the standard annotation callout. You may also change it and apply the Auto Layout constraints the way you want it to look.

Ok, let’s start by placing some annotations in the map. The annotations will represent some starbucks in the Paris area.

Select ViewController.swift file from the Project navigator view to open it in the editor. Then place the following declarations at the top, right after the class declaration:

Next, locate the viewDidLoad method and implement the code below inside:

A quick explanation is needed here:

//1: Here you just filled the arrays with some starbucks coordinates and informations, also you set the current class as the map view delegate since we will implement some MKMapView protocol methods here.

//2: Looping over the starbucks arrays, instantiating an annotation for each object and add it to the map view. You will implement the StarbucksAnnotation custom class right away.

//3: After placing all the annotations, you just centered the map around the Paris area with a convenient zoom level.

Also, don’t forget to change the class declaration like below, so that the class now conforms to the MKMapViewDelegate protocol:

Alright, time to implement the custom MKAnnotation class that will handle a reference for all the pin informations (image, phone, coordinates, etc).

Select “File\New\File” from the menu and choose the “Swift File” template. Call it “StarbucksAnnotation” and hit the “Create” button.

Erase the default content that comes with the “StarbucksAnnotation” class and implement the following code inside:

As you can see, the class should conform to the MKAnnotation protocol in order to provide any annotation-related data. In our case, the phone, coordinate, name, address, image are all relevant to the annotation and will all be attached to the starbucks pin and fetched when necessary.

Now you can build and run the project successfully. All of the three pin annotations will be correctly placed on the map view.

Switch back to the “ViewController.swift” class and implement the following MKMapViewDelegate protocol method:

The method above will be called each time an annotation is about to show in the map. Basically, it will ask for a reusable annotation from the map view. If no reusable object is available in the queue, it will instantiate a new MKAnnotationView object (instance of the “AnnotationView” class).

Note that you set the canShowCallout property to false, this is important since you gonna provide your own custom callout view for the annotation and hence you tell the system to NOT show the default provided callout.

Let’s create the “AnnotationView” class. Select “File\New\File” from the menu, choose “Swift File” template and name it “AnnotationView”. Remove its default content and replace it with the following:

The project can now compile and run correctly, but the custom callouts views can not be shown yet.

In order to add the custom view and make it behave as the annotation callout, you will need to choose the right moment to do so. The correct moment is when the annotation is selected, at such event, you will add the custom view as a subview to the annotation view 🙂

The idea is clear now, fortunately there is a ready to go MKMapViewDelegate protocol method (mapView:didSelectAnnotationView:) that will let us know each time an annotation view is selected.

Switch to the “ViewController.swift” file and implement the following code before the class closing bracket:

Some explanation is needed here:

// 1: First, the method will check whether the selected annotation is a user location related annotation, in such case, it will return without doing anything.

// 2: Here, you cast the annotation to the “StarbucksAnnotation” class in order to fetch the related data for the starbucks objects. Then, you instantiated the “CustomCalloutView” xib file and set the name/address/phone/image data to show on the view.

// 3: Finally, you centered the callout view to align it right above the annotation view.

So far so good, you can now build and run the app. The callouts will show correctly. However, you will notice that the obvious behaviour of dismissing an annotation view callout when another annotation is selected is not occurring.

To make this happen, let’s implement one last protocol method in order to deselect an annotation view and always keep one callout view shown on the map.

Always inside the “ViewController.swift” file, Implement the following method code before the class closing brackets:

The code above will test whether the deselected annotation view is an instance of the “AnnotationView” class, in which case it will run through its subviews and remove them. Remember the “CustomCalloutView” object was added as a subview of the annotation view, and this makes it behave like any other subview in the annotation views hierarchy.

That’s it, run the app and enjoy your work 🙂

As usual, the final project source code is available for download here.

I would be interested to know how do you guys customise your map annotation views’ callouts? Let me know in the comments below.

About Malek

Malek is a passionate iOS Engineer and Founder of Medigarage Studios, a small mobile games startup. I started my iOS adventures in 2011 and since then I fell in love with it. You can hire me for your project, get in touch by Email to discuss further details. Also, feel free to reach out on Twitter and Google+.

  • Joseph Hooper

    Hi, Malek! When I try to run the app on my iPhone 5, I get the “ambiguous use of subscript error” associated with the line below: let point = LocationsAnnotation(coordinate: CLLocationCoordinate2D(latitude: coordinate[0] as! Double, longitude: coordinate[1] as! Double)). Any thoughts on this? Thanks for the tutorials.

  • Malek_T

    Hi Joseph, I didn’t used a LocationsAnnotation class on the Tutorial. Can you provide me with more code from your project? Thanks!

  • Joseph Hooper

    Hi, Malek! Thank you for your reply. My apologies for uploading the code without any context. My uploaded code corresponds to your code below. It runs fine in the simulator, but when trying to run it on my iPhone 5 the message “ambiguous use of subscript error” corresponds to the line marked with an asterisks. And, the error occurs within your project and mine. Also, I only made changes to variable names throughout all my files. There’s a longer sample of the code I used below the dashed line. Thank you for your help and all of the great tutorials!

    for i in 0…2
    let coordinate = coordinates[i]
    let point = StarbucksAnnotation(coordinate: *CLLocationCoordinate2D(latitude: coordinate[0] as! Double, longitude: coordinate[1] as! Double))
    point.image = UIImage(named: “starbucks-(i+1).jpg”) = names[i]
    point.address = addresses[i] = phones[i]

    class ViewController: UIViewController, MKMapViewDelegate {
    @IBOutlet var mapView: MKMapView!
    var coordinates: [AnyObject]!
    var names:[String]!
    var infos:[String]!

    override func viewDidLoad() {

    // 1

    coordinates = [[35.91392,-79.053074],

    names = [“Battle Hall”,
    “Cobb Dorminatory”,
    “Franklin Street”,
    “Horton Residence Hall”]

    infos = [“Include info here.”,
    “Include info here.”,
    “Include info here.”,
    “Include info here.”]

    self.mapView.delegate = self

    // 2

    for i in 0…3 //This is where the issue seems to lie
    let coordinate = coordinates[i]
    let point = LocationsAnnotation(coordinate: *CLLocationCoordinate2D(latitude: coordinate[0] as! Double, longitude: coordinate[1] as! Double))
    point.image = UIImage(named: “tour-(i+1).jpg”) = names[i] = infos[i]


  • Malek_T

    Hi Joseph, please change the first two lines inside the for loop as below. This will fix the error 🙂

    let coordinate:[Double] = coordinates[i] as! [Double]
    let point = StarbucksAnnotation(coordinate: CLLocationCoordinate2D(latitude: coordinate[0], longitude: coordinate[1]))

  • Joseph Hooper

    Ah, that fixed it. Thank you, Malek! I greatly appreciate it!

  • Keyquotes

    hi, when i try to insert a func witch calls a phone number, it dosent work, i mean, when y press the label nothing happens. i downloaded your example but nothing happens again
    could you tell me any solution?
    thanks a lot

  • Darrell

    This example gives me all that I am looking for, except that the tapGesture does not work. I tried adding a function =>
    func CallPhoneNumber(sender: UITapGestureRecognizer)

    But this did not work.

    Any ideas?

  • Jr

    Hi Malek, thank you for the tutorial, it helped me A LOT!
    I am a beginner with iOS development, so excuse me if my question is obvious: can you teach us also how to dismiss the popover by tapping on it?
    Thank you again!

  • Andy

    Hi Malek

    With your suggested snippet to Joseph question the tutorial now runs on my iPhone 6 Plus, so thank you. One thing I have noticed is when you click on a pin that’s near the edge of a map, the callout isn’t fully visible. Is this problem solvable?

  • Malek_T

    Hi Andy,
    Many thanks for your comment 🙂
    I would suggest to center the map programmatically once the annotation view is selected. This way, the callout will be always fully visible.
    To do so, please add the following line of code at the end of the mapView:didSelectAnnotationView: protocol method:

    mapView.setCenterCoordinate((view.annotation?.coordinate)!, animated: true)

    Happy coding 🙂

  • Andy

    Thanks Malek and such a quick response. Could you add that feature and the code snippet that cured the compiler error to the tutorial as I am sure that will help other viewers.

    Now I would like to touch the callout itself and run some code, is this possible? So retain the deselection elsewhere in the view but either add a button or right side disclosure used in the standard callout.

  • MR

    Hi Malek,
    10x for the tutorial.
    Actually, I need your help..

    I add a button to the customCalloutView.xib and i take her outlet and action.
    In the ViewController.swift , in the didSelectAnnotation method , i add :
    “calloutView.clickBtn.addTarget(self, action: #selector(, forControlEvents: .TouchUpInside)”
    where i define the “click” function in “ViewController.swift” as following :
    ” func click(){
    let ac = UIAlertController(title: “hi”, message: “hiiii”,
    preferredStyle: .Alert)
    ac.addAction(UIAlertAction(title: “OK”, style: .Default, handler:
    presentViewController(ac, animated: true, completion: nil)

    but nothing happens…

    plz help , how i make the action of button works ?

  • Kelvin

    Recently Apply introduced Swift3 and there are some changes that are not applicable. Would you mind updating it please? The problem I am getting is at your coordinate, I got an error that says, Contextual type AnyObject cannot be used with with array literal.

    coordinates = [[48.85672,2.35501],[48.85196,2.33944],[48.85376,2.33953]]

    Thank you for your tutorial. I love them

  • Malek_T

    Hi Kelvin, I just updated the tutorial and its source code projects to Swift 3.
    Happy coding 🙂

  • Kelvin

    Quick question! When the custom annotations pop up, you tap once and the custom annotation will disappear, I also noticed that you implement a tapgesturerecognizer which however doesn’t call the number on the custom annotation because when you tried tapping on the number, the custom annotation disappear. Please advise