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 code above is necessary to track the touch click on the custom callout views since you will no longer use the default annotation view callout, but instead a custom UIView 🙂

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. You also created a button with the same frame as the phone label in order to respond to the click and fire a method to launch the phone call.

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

Implement the selector method that gets running when you click on the phone label:

The code from the method above will cast the selected view to retrieve the phone number from the CustomCalloutView property.

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”)
    point.name = names[i]
    point.address = addresses[i]
    point.phone = phones[i]
    self.mapView.addAnnotation(point)

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

    override func viewDidLoad() {
    super.viewDidLoad()

    // 1

    coordinates = [[35.91392,-79.053074],
    [35.912409,-79.044708],
    [35.914558,-79.053252],
    [35.903131,-79.043632]]

    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”)
    point.name = names[i]
    point.info = infos[i]
    self.mapView.addAnnotation(point)

    }

  • 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)
    {
    print(“Hello”)
    }

    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(ViewController.click), forControlEvents: .TouchUpInside)”
    where i define the “click” function in “ViewController.swift” as following :
    ” func click(){
    print(“clickeddd”)
    let ac = UIAlertController(title: “hi”, message: “hiiii”,
    preferredStyle: .Alert)
    ac.addAction(UIAlertAction(title: “OK”, style: .Default, handler:
    nil))
    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

  • Fabrice

    Hello Malek, thank you for this tutorial which is very interesting, especially when you learn. I have two questions 1 / How to click on an anotation to get to a view of my application. 2 / How to put a different image for “annotationView? .image” starbucks “”. Thanks for your feedback.

  • Andrew Spencer

    Hey @Malek_T:disqus , a few others have commented about the touch gesture recognizer not being called correctly. I’m having the same problem. Could you share some advice on how to fix it with us?

  • Malek_T

    Hi Andrew, I have edited the tutorial and the related project to answer your question 🙂
    Enjoy and don’t forget to subscribe!

  • Malek_T

    Hi Fabrice,

    I have edited the tutorial as well as the project source code to demonstrate how to track clicks on the callout views. Enjoy 🙂

  • Malek_T

    Hi Kelvin,

    Sorry for that, I have edited the tutorial as well as the project source code to demonstrate the callout clicks. Enjoy 🙂

  • Malek_T

    Hi Andy,

    I have edited the tutorial as well as the project source code to show you how to track click events on the callout subviews. Enjoy 🙂

  • Fabrice

    Hello thank you for this return, because of my bad English I do not know if I was clear in my message. In fact following your tutorial, I created a card with 5 pins and visiblent at the same time. Each pin to an annotation with a button that must return to a view corresponding to each annotation. In fact I think I was mistaken in asking the question, but I wanted to know if you can help me to define the segues to see the different views. Sorry and thank you

  • Cory Billeaud

    Wow, @Malek_T:disqus I just want to say amazing. I have been working on trying to get hitTest to work on my MKAnntotationView for the last cpl of days from old tutorials and I was ready to give up for a few days and you just updated this tut a few hours ago. So I finally got that to work correctly. Yet I do have a question. Following your tutorial, instead of using the addTarget to get the function to call a number. I want to use the addTarget to get driving directions from my annotation, which is using the CustomAnnotation that has coordinates that are pulled from an array on firebase. So how would I use the button.addTarget(self, action: Selector(“”) for, .touchUpInside) to use the annotation make that happen.

    Its funny how you can run in circles trying to find a solution to a problem, and then a few hours later another piece of the puzzle emerges. It was if you were reading my mind this morning. Thanks again for your work.

  • Cory Billeaud

    well I got that addTarget to work

    button.addTarget(self, action: #selector(MapViewVC.getDirections(sender:)), for: .touchUpInside)

    I didn’t realize you were calling the ViewController called ViewController.

  • Cory Billeaud

    And I made some more progress. So I created a variable
    var selectedAnnotation: MyCustomAnnotation!

    and in the didSelect func
    self.selectedAnnotation = view.annotation as? MyCustomAnnotation

    so now when you click on the addTarget function it can now grab the selected annotation coordinates in the custom func, which yours is the
    func callPhoneNumber.

    This is in case anyone was needing the coordinates instead of just the number. Almost there. Your updated tutorial really helped today.

  • Andy

    Hi Malek

    Thanks for updating tutorial and source code, I have downloaded it and all works great, thank you very much. I will now try and incorporate some of the features in my app.
    Regards Andrew

  • Malek_T

    You welcome Andy 🙂

  • Malek_T

    Thanks a lot Cory. I am glad to help 🙂

  • Cory Billeaud

    Well thank you, like I said originally, I spent the last few days trying to hitTest to work from old swift tutorials and stack overflow questions to no avail. This updated tutorial of yours solved my issue.

  • He

    HI this doesn’t seem to work

  • Fabrice

    Thank you Malek for this return. How for each annotation by clicking on it can you go to differentq viewcontroller ?. thank you

  • Никита Икрамов

    Hi! I have got a problem with button action.Button doesnt clicks in simulator, but it can be clicked programmatically. Here is a fragment of code in didselectAnnotation view.

    let starbucksAnnotation = view.annotation as! CustomAnnotation
    let views = NSBundle.mainBundle().loadNibNamed(“callOutView”, owner: nil, options: nil)
    let calloutView = views?[0] as! CustomCalloutView
    calloutView.botOption.text = “test”
    calloutView.topOption.text = “Babysitting”
    let button = UIButton(frame: calloutView.frame)
    button.userInteractionEnabled = true
    button.addTarget(self, action: #selector(self.buttonClicked(_:)), forControlEvents: UIControlEvents.TouchUpInside)
    button.backgroundColor = UIColor(red: 102/255, green: 250/255, blue: 51/255, alpha: 0.5)
    calloutView.addSubview(button)
    self.delay(2) {
    button.sendActionsForControlEvents(.TouchUpInside)
    }

  • Fabrice

    Hello Malek, I looked at your example, but when I click on an annotation I will not go on another viewcontroller? Thanks for your feedback

  • ABDULLAH ANSARI

    I am unable to click on callout view. I tried so much but i couldn’t get that. Can you suggest me?

  • William B Travis

    Malek.. Very helpful. Thank you for taking the time to do this and for your generosity in sharing.

  • paterik

    Hi Malek,

    thank you this tutorial !!! I’ve spend hours in evaluating this problem and finally find your (great) solution … I’m deeply impressed 🙂

  • Malek_T

    I am glad to help William 🙂

  • Malek_T

    Thanks Paterik 😉

  • Юлия Кордюкова

    this code doesn’t work for annotations. All i have on my map are 3 pins and no additional subviews when clicking on pins. Please upgrade your code.

  • Lukas Bimba

    How could I make this work for MKLocalSearch results? I am unsure how to wire the MKPlaceMark and MKMapItem data to my custom call out

  • David Lari

    Apple made this so hard. Thanks for figuring it out and sharing with everyone.