[Swift MapKit Tutorial Series] How to Display and Manage Different Custom Pin Annotations View Objects on the Map

Update April 2016: Fully updated for iOS 9.3 (Xcode 7.3 and Swift 2.2).

Lot of stuff was demonstrated in the first and second tutorials of this series, you learned how to search any place or POI and how Apple maps API got lot smarter analysing the human query even for uncompleted addresses. You also worked with the great MKMapItem API to discover the place details, and along the way, you did great effort implementing the presentation controller with the pop over context in iOS 8.

In this tutorial, you will work with the world bank API to get useful data about the income level of different countries around the world.

Along the way, I gonna show you how to display and manage multiple and different pin annotations on the map, and how to customise the default pin view with a custom image. The data will be pulled remotely from the world bank API with a HTTP request, so you will work with the neat NSURLSession class to get that right in your app.

By the end of this tutorial, you can have a useful app targeting a specific audience and you can submit it to the Apple Store if you want 🙂

Note: This tutorial is independent from the two previous tutorials, you don’t have to read them to build the app in this one, here you will do everything from scratch.

Let’s do this!

Open up Xcode, select ‘File\New\Project’ from the menu. Choose ‘Single View Application’ from the templates list in the ‘iOS\Application’ tab. Name the project ‘WorldIncomeLevel’ and make sure the language is Swift and that your app will be targeting all devices by selecting ‘Universal’ from the ‘Devices’ menu.

Cool, now we are going to set up the simple UI for the project. Basically, there will be a button, a navigation bar and a map to display all pin annotations for the income level countries. On button click, a pop over list will be shown to select the area in the world you want to show the income level for.

Select Main.storyboard file from the Project navigator, then click on the view controller from the Document Outline area.

Select the View Controller from the Document Outline Area

Now select ‘Editor\Embed In\Navigation Controller’ from the menu. We need to embed the view scene in a navigation controller to take advantage of the navigation bar in which you gonna put the bar button right away.

From the Object library, drag a UIBarButtonItem and place it to the left of the navigation bar in the view controller scene.

Now switch to the ‘Attributes inspector’ view and change the System Item property of the bar button item to ‘Add’.

Also, select the navigation bar and change its title to ‘Income Level Map’ from the ‘Attributes inspector’ view.

Here is how the navigation bar should look like:

Add bar button item to the navigation bar and change its title

Good, now drag a Map Kit View object from the Object library to the view controller scene, select the ‘Size inspector’ view and set its X to 0, Y to 64, Width to 600 and Height to 536.

Select the Pin menu, make sure the ‘Constrain to margins’ checkbox is UNchecked and activate the top, leading, bottom and trailing spacing constraints as shown in the following screenshot.

Working with the Pin menu in Auto Layout

Don’t forget to select the ‘Add Constraints’ button in the Pin menu to apply the Auto Layout changes.

Now, let’s hook up the objects to the class in order to manipulate them from within the code. To do so, switch to the ‘Assistant editor’ and follow the steps below:

1/ Ctrl+drag from the bar button item to the view controller (just below the class name), make sure it’s an ‘Outlet’ from the Connection drop down list and name it ‘areaListBtn’.

2/ Ctrl+drag from the map view to the view controller (just below the class name) and select ‘Outlet’ from the Connection drop down list. Name it ‘mapView’ and click the ‘Connect’ button.

3/ Ctrl+drag again from the bar button item to the view controller (just below the class name). This time, make sure it’s an ‘Action’ from the Connection drop down list and name it ‘showAreaList’.

Don’t forget to import the MapKit framework by placing the following line above the class name in the ViewController.swift file:

So far, the class looks like the following:

Last thing before you move to coding is to import some images to the project for the custom pins annotations in the map.

We will be dealing with four different income levels, so there will be four custom icons, start by downloading them here.

The best way to import images to Xcode is within the asset catalog. First select ‘Assets.xcassets’ from the ‘Project navigator’, then click the ‘+’ button at the bottom of the window and select ‘New Image Set’ from the list. Name the image set ‘High income’. Now just unzip the folder you downloaded earlier and drag the image ‘HI@2x.png’ to the 2x placeholder in the image set, also drag ‘HI@3x.png’ to the 3x placeholder.

Repeat the same steps to make three more image sets, name them ‘Lower income’, ‘Lower middle income’ and ‘High middle income’, and import the images respectively from the folder to their placeholders.

The final asset catalog should look something like this:

Xcode Asset catalog

Cool, now time to code and get this app to work 🙂

Select ViewController.swift from the Project navigator view and add the following variable declarations right after the class name:

As you may know, ‘contentController’ will handle the content inside the pop over controller, whereas, ‘areaListTable’ is the table view which will be embedded inside the content controller. All areas will be shown in a table view within a list style, the table data source will be the ‘regionNames’ array, while the ‘selectedItemIndex’ variable will store the selected region area from the table view. The ‘regionCodes’ array contains all region codes, this is useful to construct the url request later on, since the region code is a parameter of the requested API.

Next, locate the viewDidLoad function and add the following code inside (right after the super.viewDidLoad statement):

Basically, this will initialise the content table view controller and embed the table view object inside it. Setting the ‘selectedItemIndex’ to -1 is important since it happens that the user can select no item, in such case, this variable will be storing a value other than 0 (being 0 is the first index in the table view).

Let’s make the class adopt the table view data source and delegate protocols as well as the map view delegate protocol, update the class name to look like the following:

Now, let’s put in the data source protocol code for the table view to load its data, place the following code before the closing bracket of the class:

This is the required data source implementations, data is being extracted from the ‘regionNames’ array as we talked about earlier.

Next, locate the ‘showAreaList’ function and place the following code inside:

This will create and present a pop over controller to show after the click on the bar button. Don’t forget to adopt the class to the UIPopoverPresentationControllerDelegate protocol, to do so, update the class name with the following:

So far, if you run the app on iPad, you will notice the pop over is shown correctly since it has enough space to draw the pop over layout on the screen. However, if you run on iPhone screens, the pop over will show up as a modal screen instead. This is cool, but the modal controller is missing a top bar to prevent content from overlapping underneath the status bar, so let’s add that along with a dismiss button.

Before the closing bracket of the class, place the following code:

This will ask the delegate to display the presented content on full screen mode for all compact width screens (like iPhone portrait). It will also embed the presented content into a navigation controller and add a right bar button to dismiss the modal controller manually.

Before you run the app, let’s quickly add the ‘done’ function code which will be called after you click on the bar button, to dismiss the modal controller.

Place the following code somewhere in the class:

Run the app and make sure the content is displayed, and correctly dismissible on all devices and orientations.

Let’s add some UITableViewDelegate protocol methods to show the user selection on the list. Place the following code somewhere in the class before the closing bracket:

This will add a nice checkmark to the selected item in the table view. Also, the didDeselectRowAtIndexPath method will ensure that there will be only one checkmark at a time in the list, since the API will handle one area search at a time.

Run the app on iPad simulator and try to select an item from the table view, dismiss the pop over and then load it again, you may notice the previous selection is not cleared. This is because the table view in the pop over context is not refreshed implicitly, so you have to reload it manually to clear the previous selection. To do so, place the following code before the closing bracket of the class:

So far so good, the content is presented correctly, but it needs to be interactive with the user selection. The normal behaviour is to get the selected row in the presented list and, based on it, the app will search “local income” data for that area in the world.

The starting point of all the mapping work will be after the user dismissing the presented content, at that time, the app will start a networking process and map displaying of pins.

Locate the ‘done’ function you implemented earlier and add the following statement before its closing bracket:

Note: Unlike on compact width screens, the presented pop over controller content doesn’t have a button to dismiss when you run the app on iPad screen (regular width). So in this case, you have to detect the dismiss event of the pop over controller and react accordingly. To do so, just add the following code inside the class:

Now time to implement the mapSearch function, go ahead and place the following code before the closing bracket of the view controller class:

This will check whether the user has actually selected an item from the list, then it will construct a url with the chosen area and request data via the NSURLSession dataTaskWithURL API. The rest of the code is just a JSON converting stuff since we cannot deal with pure Foundation object, hence you converted it to JSON with the NSJSONSerialization.JSONObjectWithData API. Finally, you called another function to display the income level regions in the map.

We are almost done, let’s implement the displayRegionIncomeLevel method which will put together all map pins for the income level area. Place the following code inside the class body:

Let’s break down the code above:

//1: The data were fetched and stored in an array, the map will set its center based on the first region element longitude/latitude in the array.

//2: You are looping through the array items, creating a point annotation object for each one and assigning a custom image name with the CustomPointAnnotation class (you will implement this class right away). We have to differentiate between pins, to know which one will get which custom pin view, that’s why subclassing the MKPointAnnotation class was very important to set some sort of image title and assign it on the delegate protocol method ‘viewForAnnotation’ which you will implement later.

//3: Finally, adding each annotation view to the map after setting the coordinate of the point annotation with a title and a subtitle to show to the user after he clicks on the pin.

Before you implement the CustomPointAnnotation class, let’s finish up with this controller by implementing one last method, ‘viewForAnnotation’ is an MKMapViewDelegate protocol method which gets called for each pin to display on the map. It’s kind like cellForRowAtIndexPath for the table view context. We need to implement the viewForAnnotation protocol method because it’s the best place to customise the pin view and assign a custom image to it.

Go ahead and place the following code before the closing bracket of the class:

This will dequeue and return a reusable annotation for the given identifier, and if it’s nil, then a new annotation view is created and assigned the image from the custom point annotation object discussed earlier. As you may guess, we are casting the annotation to the CustomPointAnnotation class in order to access the image name of the pin.

Let’s finish up the work and create the CustomPointAnnotation class, select ‘File\New\File’ from the menu. Select ‘Cocoa Touch Class’ from the templates list and name it ‘CustomPointAnnotation’, also make sure it’s a subclass of ‘MKPointAnnotation’

MKPointAnnotation subclass

Select it from the Project navigator view and change its content to look like the following:

That’s it, run the app, zoom in and out the map to better see the annotations for some areas, and enjoy your work 🙂

Important: As of iOS 9, App Transport Security is blocking some communications between the app and web services, so you need to disable ATS for our app here to be able to communicate with the World Bank API. To do this, select Info.plist file from the Project navigator, add a new NSAppTransportSecurity Dictionary item to the root dictionary, then add an NSAllowsArbitraryLoads key and set its value to YES, as shown below:

App Transport Security

As usual, you can download the final project here. Feel free to leave a comment. I would love to hear your thoughts!

MapKit custom MKPointAnnotation view

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+.

  • Pingback: Android & iOS Application Development | Just another My blog Sites site()

  • Mike Horner

    I found this really useful – thanks for taking the time!

  • Malek_T

    Thanks Mike, you are welcome!

  • fan

    Great tutorial, thank you!

  • Malek_T

    I am born to help, you are welcome 🙂

  • Haiyan

    Super Cool!! It was easy to follow and very powerful! Thank you so much!

    I spotted a small error introduced by formatting:

    This ®ion has to be changed to &region

  • Haiyan

    I found that the app crashes after switch quickly between different areas by clicking the tableview cells.

    This error message appears:

    WorldIncomeLevel[3057:955514] This application is modifying the autolayout engine from a background thread, which can lead to engine corruption and weird crashes. This will cause an exception in a future release.

    I found the solution from http://stackoverflow.com/questions/28302019/getting-a-this-application-is-modifying-the-autolayout-engine-error.

    we only need to put the part which modifies the UI in the main queue

    dispatch_async(dispatch_get_main_queue(), {
    self.displayRegionIncomeLevel(dataArray as [AnyObject])

  • Malek_T

    Hi Haiyan 🙂

    Thanks for your comment and for the catch. All UI tasks should be wrapped in the main thread, and since the dataTaskWithURL API is asynchronous. A dispatch block was needed to move to the main thread and perform all UI tasks. I just updated the code 🙂

  • Fabrice

    Hello, is it possible to customize annotations for when clicked directs them to a page of the application

    Thank you


  • Malek_T

    Hi Fabrice 🙂
    Sure, you can use the calloutAccessoryControlTapped protocol method to respond to clicks on the annotation. From the method, you can call a segue with a given identifier to move to another screen.

    Hope this helps!

  • disqus_jHqoh0tuP2

    Love your tutorials..! Excellent.
    I had errors in the section //2 until I looked at the final file download. It differs from the text above when creating the dictionary.
    let obj = item as! Dictionary
    The angled brackets and included text aren’t shown above. Same again a few lines down:
    let incomeLevel:Dictionary = obj[“incomeLevel”] as! Dictionary.
    Now, no errors but I’m not able to pin anything in the simulator app. Probably the plist thing (there was no NSAppTransportSecurity and it reverts to AppTransportSecurity when I type it in) or else I’m being dull (known to happen).

  • beobjective


    The WS url seems to be wrong. Can u share the working one ?

  • beobjective


    Can u share the whole working url please?


  • Malek_T

    Hi 🙂
    Thanks for the catch. Fixed!
    It seems the angle brackets were treated as html tags. I will try to disable that behaviour asap.
    For the ATS, it’s working fine on the project source code attached above.

  • Malek_T

    Hi 🙂
    I double checked it. The API url is valid and seems to return correctly.
    Let me know

  • beobjective

    Hi Malek,

    I try ;


    on browser for demonstration of


    the api returns

    [{“message”:[{“id”:”120″,”key”:”Parameter ‘per_page’ has an invalid value”,”value”:”The provided parameter value is not valid”}]}]

    Am ı missing about ®ion ??

    thanks for your tut and reply.

  • beobjective

    as u seen the link is broken on here too. It s not underlined.

  • Malek_T
  • Krystian

    Malek, I really want to thank you so much for this tutorial. It has helped me SO MUCH in my project. I really feel the need to answer a few questions for some of your readers.

    Fabrice, it is very possible and easy to do. Just create a segue from your source (current) view controller to your destination view controller (call it “myCustomSegue” for example) and ADD this (not replace) below the “func mapView…” part of this tutorial:

    func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
    self.performSegueWithIdentifier(“myCustomSegue”, sender: self)

    Alternatively, you can also incorporate the segue into a UIAlert by adding an action item to it, such as this:

    func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
    let mapItem = view.annotation as! CustomPointAnnotation
    let mapItemTitle = mapItem.title
    let mapItemSubTitle = mapItem.subtitle

    let alert = UIAlertController(title: mapItemTitle, message: mapItemSubTitle preferredStyle: .Alert)
    alert.addAction(UIAlertAction(title: “Details”, style: UIAlertActionStyle.Default, handler: { action in
    self.performSegueWithIdentifier(“myCustomSegue”, sender: self)
    alert.addAction(UIAlertAction(title: “Ok”, style: .Default, handler: nil))

    presentViewController(alert, animated: true, completion: nil)

    I hope that helps! 🙂

  • beobjective

    Aff. thank u bro! 🙂

  • illustratorByDay

    Thanks, so much, Malek. I love that you were born to help..! I finally got the pins to work, too…. yay…!! (can’t remember how, but I did..!)

  • Malek_T

    You welcome, any time 🙂

  • Malek_T

    You welcome 🙂

  • dimitest

    Hello, thank you for this very helpful tutorial. I have tried to adapt my site Json data so that I can map that instead of the World Bank data from my site url. I have tried matching it, eg using regionCodes and regionNames etc. But I’m getting an error message. Please what are all the data fields should I be seeing/using in my Json, so that this app can pull my data?

  • Fabrice

    Hello, I am trying to learn and to do tests by following your tutorial. By cons I have this error (below) when I click on a region in the table. Do you have an idea. thank you https://uploads.disquscdn.com/images/da070846e75da53b16415f0d9c0b36c8d718a4d6ac4a94dbd10149ed4d1b41a7.png

  • Fabrice

    Thanks for this tutorial, I have a problem when I click on a region in the table I have the following message

  • Fabrice

    Hello, I think I need a little help. I tried to do as you told me, but it gives nothing. Here is my code to make it clearer. For indication on my map I have different annotations and from each annotation I want to go on a different viewcontroller. If you have advice I am taker. thank you in advance

    mport MapKit
    import UIKit
    import CoreLocation

    class MapViewController: UIViewController, UISearchBarDelegate, MKMapViewDelegate,CLLocationManagerDelegate {

    @IBOutlet weak var MapView: MKMapView!

    var LocationManager: CLLocationManager!

    var searchController:UISearchController!
    var annotation:MKAnnotation!
    var localSearchRequest:MKLocalSearchRequest!
    var localSearch:MKLocalSearch!
    var localSearchResponse:MKLocalSearchResponse!
    var error:NSError!
    var pointAnnotation:MKPointAnnotation!
    var pinAnnotationView:MKPinAnnotationView!

    var coordinates: [[Double]]!
    var name:[String]!

    let regionRadius: CLLocationDistance = 1000

    @IBAction func showSearchBar(_ sender: Any) {

    searchController = UISearchController(searchResultsController: nil)
    searchController.hidesNavigationBarDuringPresentation = false
    self.searchController.searchBar.delegate = self
    present(searchController, animated: true, completion: nil)

    override func viewDidLoad() {

    self.MapView.delegate = self

    var Kusumba = CustomPointAnnotation()
    Kusumba.coordinate = CLLocationCoordinate2DMake(-8.550436, 115.481009)
    Kusumba.title = “Kusumba”
    Kusumba.subtitle = “Village de pêcheur et de paludiers”
    Kusumba.imageName = “PetitPtVie”

    var Goa = CustomPointAnnotation()
    Goa.coordinate = CLLocationCoordinate2DMake(-8.551077, 115.474824)
    Goa.title = “Goa Lawah”
    Goa.subtitle = “La grotte des chauves souris”
    Goa.imageName = “PetitPtculture”

    var Andakasa = CustomPointAnnotation()
    Andakasa.coordinate = CLLocationCoordinate2DMake(-8.513608, 115.474698)
    Andakasa.title = “Andakasa”
    Andakasa.subtitle = “Pura Luhur Andakasa”
    Andakasa.imageName = “PetitPtculture”

    var Padang = CustomPointAnnotation()
    Padang.coordinate = CLLocationCoordinate2DMake(-8.530652, 115.509299)
    Padang.title = “Padang bai”
    Padang.subtitle = “Village portuaire ”
    Padang.imageName = “PetitPtplage”


    let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: -8.670458199999999, longitude: 115.2126293), span: MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1))
    self.MapView.setRegion(region, animated: true)


    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{

    print(“delegate called”)

    if !(annotation is CustomPointAnnotation) {
    return nil

    let reuseId = “test”

    var AnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
    if AnnotationView == nil {
    AnnotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
    AnnotationView?.canShowCallout = true

    let rightButton: AnyObject! = UIButton(type: UIButtonType.detailDisclosure)
    AnnotationView?.rightCalloutAccessoryView = rightButton as? UIView
    else {
    AnnotationView?.annotation = annotation


    func didReceiveMemoryWarning() {


    let CustomPointAnnotation = annotation as! CustomPointAnnotation
    AnnotationView?.image = UIImage(named:CustomPointAnnotation.imageName)

    return AnnotationView

    class CustomPointAnnotation: MKPointAnnotation {
    var imageName: String!

    override func didReceiveMemoryWarning() {
    // Dispose of any resources that can be recreated.

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar){

    dismiss(animated: true, completion: nil)
    if self.MapView.annotations.count != 0{
    annotation = self.MapView.annotations[0]

    localSearchRequest = MKLocalSearchRequest()
    localSearchRequest.naturalLanguageQuery = searchBar.text
    localSearch = MKLocalSearch(request: localSearchRequest)
    localSearch.start { (localSearchResponse, error) -> Void in

    if localSearchResponse == nil{
    let alertController = UIAlertController(title: nil, message: “Place Not Found”, preferredStyle: UIAlertControllerStyle.alert)
    alertController.addAction(UIAlertAction(title: “Dismiss”, style: UIAlertActionStyle.default, handler: nil))
    self.present(alertController, animated: true, completion: nil)

    self.pointAnnotation = MKPointAnnotation()
    self.pointAnnotation.title = searchBar.text
    self.pointAnnotation.coordinate = CLLocationCoordinate2D(latitude: localSearchResponse!.boundingRegion.center.latitude, longitude: localSearchResponse!.boundingRegion.center.longitude)

    self.pinAnnotationView = MKPinAnnotationView(annotation: self.pointAnnotation, reuseIdentifier: nil)
    self.MapView.centerCoordinate = self.pointAnnotation.coordinate

    func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {

    //if control == view.rightCalloutAccessoryView {

    if (title == “Kusumba”) {
    self.performSegue(withIdentifier: “Kusumba”, sender: self)

    } else if (title == “Goa Lawah”){
    self.performSegue(withIdentifier: “Goa Lawah”, sender: self)

    } else if (pointAnnotation.title == “Andakasa”){
    self.performSegue(withIdentifier: “Convertisseur”, sender: self)

    else {


    func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

    if (segue.identifier == “Kusumba”) {
    _ = segue.destination as! KusumbaViewController

    } else if (segue.identifier == “Goa Lawah”) {
    _ = segue.destination as! GoaLawahViewController

    } else if (segue.identifier == “Andakasa”) {
    _ = segue.destination as! AndakasaViewController