MKMapView – Adding Pins to Map and Showing Annotations

By | September 15, 2013
JFMapViewExample

I am currently in the process of writing a revised version of my iOS application (Location+), the new version is going to be totally rewritten using the new toolset, Location+ was built in the days of release/retain and xib’s instead of storyboards so it was time for a revamp.

Firstly, the source code for this tutorial can be found at the following address: https://github.com/jfield44/JFMapViewExample

An important feature of any mapping application is the ability to the let us user interact with points on the map, for example drop a pin to find the address at that point. In iOS the primary way of doing this is to add an ‘Pin’ to the map. I am going to be recreating this functionality inside my new app shortly so I thought I would put together a simple tutorial that you can follow along with if you are new to iOS or MapKit.

This tutorial will cover:

  • Adding a MapView to your application
  • Adding a Pin to the map at the user’s touched location
  • Adding a Pin at the capital city of each country in the world (We will parse a JSON file for this)
  • Showing a ‘Callout Bubble’ containing the name of the country and its capital city

N.b: This tutorial will be written using xCode 5 and iOS 7

Firstly, create a new Project in xCode, for the purposes of this tutorial choose a Single View ApplicationScreen Shot 2013-09-14 at 09.19.52

 

Click next and name the project whatever you want to name it, also set an organisation name and company identifier, these will probably just be your name.

Screen Shot 2013-09-14 at 09.22.27

 

Click next and then save Project to a directory of your choice, personally I save mine to Dropbox.

Once you have saved the Project you should see a screen like this.

Screen Shot 2013-09-14 at 09.25.17

 

This is the main configuration page for the Project, we will be using the MapKit framework today so the next step is to add that framework to the Project, to do this we goto the ‘Build Phases’ section at the top of the screen. One the Build Phases screen, click ‘Link Binary with Libraries’ and then press the + icon. Then add the MapKit.framework to the Project by selecting it and pressing Add.

Screen Shot 2013-09-14 at 09.27.46

 

Now that we have the MapKit framework added, we can start creating our MapView that will act as the Canvas for us to add our Pins. In the left hand column of our Project click on the file ViewController.h , or if you have added a class prefix then (PREFIX)ViewController.h.

Objective-C frequently uses the Delegation Design Pattern, you can read more about this here , we need to implement a number of MKMapView delegate functions in order for us to achieve the functionality that we are looking for. In addition to this, you may wish you implement more MKMapView Delegate functions for example responding to the Map Starting to Load. For us to become an MKMapView Delegate we need to the following code into ViewController.h .

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

@interface JFViewController : UIViewController <MKMapViewDelegate>

@property (weak, nonatomic) IBOutlet MKMapView *mapView;

- (IBAction)addCitiesToMap:(id)sender;

@end

The above code tells the MapView that we will confirm to the delegate protocol, we have also created an outlet view to the Map, you should connect this up by dragging a MapView object onto your Storyboard View. We have also created an IBAction which will allow us to add cities to the map later on. Add a Navigation Bar and Bar Button Item, after that you should link up the AddCitiesToMap function to the Bar Button Item.

Screen Shot 2013-09-14 at 15.37.06

 

Now that we have our MapView hooked up to our code, we need to explicitly tell the MapView that our ViewController will be the delegate. Add the following code to the ViewDidLoad method method inside of ViewController.m (.m not.h , we are now looking at the implementation).

#import "JFViewController.h"

@interface JFViewController ()

@end

@implementation JFViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.mapView setDelegate:self];
}

- (IBAction)addCitiesToMap:(id)sender{
    //Lets fill this in later
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

The next step for us is to add a Gesture Recogniser to the MapView, this will detect when the user touches the map and allow us to respond based on the nature of the interaction and the amount of time the map was touched for. To do this, create another method called AddGestureRecogniserToMapView in ViewController.h and then copy the code below into your ViewController.m. Your ViewController.m should now look like this.

//
//  JFViewController.m
//  JFMapViewExample
//
//  Created by Jonathan Field on 14/09/2013.
//  Copyright (c) 2013 Jonathan Field. All rights reserved.
//

#import "JFViewController.h"

@interface JFViewController ()

@end

@implementation JFViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.mapView setDelegate:self];
    [self addGestureRecogniserToMapView];
}

- (void)addGestureRecogniserToMapView{
    
    UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc]
                                          initWithTarget:self action:@selector(addPinToMap:)];
    lpgr.minimumPressDuration = 0.5; //
    [self.mapView addGestureRecognizer:lpgr];
    
}

- (void)addPinToMap:(UIGestureRecognizer *)gestureRecognizer
{
    
    if (gestureRecognizer.state != UIGestureRecognizerStateBegan)
        return;
    
    CGPoint touchPoint = [gestureRecognizer locationInView:self.mapView];
    CLLocationCoordinate2D touchMapCoordinate =
    [self.mapView convertPoint:touchPoint toCoordinateFromView:self.mapView];
    
    JFMapAnnotation *toAdd = [[JFMapAnnotation alloc]init];
    
    toAdd.coordinate = touchMapCoordinate;
    toAdd.subtitle = @"Subtitle";
    toAdd.title = @"Title";
    
    [self.mapView addAnnotation:toAdd];
    
}

- (IBAction)addCitiesToMap:(id)sender{
    //Lets fill this in later
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

If you run the simulator now, you will see that the MapView appears and you can also touch and hold the Map for a short while and a Pin will appear. This functionality is contained in our addPinToMap function. The Gesture Recogniser determines that a touch has been made for more than 0.5 seconds and then triggers a call to the addPinToMap function, this then calculates the coordinates for which the pin should be added based on the Touch Coordinates of the device. In order for us to add the Pin to the map, we had to create a new subclass of NSObject that confirms to the <MKAnnotation> protocol, the code for this is as follows.

New Class JFMapAnnotation.h

//
//  JFMapAnnotation.h
//  JFMapViewExample
//
//  Created by Jonathan Field on 14/09/2013.
//  Copyright (c) 2013 Jonathan Field. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface JFMapAnnotation : NSObject <MKAnnotation>{
    
    NSString *title;
    NSString *subtitle;
    NSString *note;
    CLLocationCoordinate2D coordinate;
}

@property (nonatomic, copy) NSString * title;
@property (nonatomic, copy) NSString * subtitle;
@property (nonatomic, assign)CLLocationCoordinate2D coordinate;

@end

New Class JFMapAnnotation.m

//
//  JFMapAnnotation.m
//  JFMapViewExample
//
//  Created by Jonathan Field on 14/09/2013.
//  Copyright (c) 2013 Jonathan Field. All rights reserved.
//

#import "JFMapAnnotation.h"

@implementation JFMapAnnotation

@synthesize title;
@synthesize subtitle;
@synthesize coordinate;

@end

Our next step will be to create the functionality in order for our application to add pin’s at capital cities across the world, we have a .json file that contains this data stored in the following format. We could have used a plist file but it is good practise to use a cross platform file format and it is the typical response that we would get if we were working with a web api.

{
    "Country":"Afghanistan",
    "Capital":"Kabul",
    "Latitude":34.52,
    "Longitude":69.18
  }

To Parse the JSON file and convert the raw JSON objects into Objective-C Foundation Objects, we will create another function that will be called from our addCitiesToMap: function. In addition to this, we will modify our addCitiesToMap: function to use both the Main and Background threads. While this is not necessary for parsing this small amount of data, it is good practise in order to prevent the GUI from being locked when doing lots of work on the Main thread.

/*
 On the background thread, retrieve the Array of Annotations from the JSON from the next function.
 On the main thread, add the annotations to the map.
 */
- (IBAction)addCitiesToMap:(id)sender{
    
    __block NSArray *annoations;
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        annoations = [self parseJSONCities];
        
        dispatch_async(dispatch_get_main_queue(), ^(void) {
            
            [self.mapView addAnnotations:annoations];
            
        });
    });
    
}

/*
 Convert raw JSON to Objective-C Foundation Objects
 Iterate over each returned object and create a JFMapAnnotationObject from it
 Add each new Annotation to an Array and then return it.
 */
- (NSMutableArray *)parseJSONCities{
    
    NSMutableArray *retval = [[NSMutableArray alloc]init];
    NSString *jsonPath = [[NSBundle mainBundle] pathForResource:@"capitals"
                                                         ofType:@"json"];
    NSData *data = [NSData dataWithContentsOfFile:jsonPath];
    NSError *error = nil;
    NSArray *json = [NSJSONSerialization JSONObjectWithData:data
                                              options:kNilOptions
                                                error:&error];
    
    for (JFMapAnnotation *record in json) {
        
        JFMapAnnotation *temp = [[JFMapAnnotation alloc]init];
        [temp setTitle:[record valueForKey:@"Capital"]];
        [temp setSubtitle:[record valueForKey:@"Country"]];
        [temp setCoordinate:CLLocationCoordinate2DMake([[record valueForKey:@"Latitude"]floatValue], [[record valueForKey:@"Longitude"]floatValue])];
        [retval addObject:temp];
        
    }
    
    return retval;
}

Once you have implemented this, make sure to hook up your IBAction addCitiesToMap: to the Bar Button Item or any other button on your Storyboard. When you press that button, you should now see that a pin has been added at every capital city.

JFMapViewExampleIphone

I hope this serves a a simple introduction to MapKit and also reading and parsing JSON files.

The full project can be downloaded from https://github.com/jfield44/JFMapViewExample

To request more tutorials or ask questions visit http://jonathanfield.me

 

26 thoughts on “MKMapView – Adding Pins to Map and Showing Annotations

  1. Quinten

    Hi Jon, thanks for the wonderful help it works beautifully. I have another question. How can I have the points on the map load automatically. Also is there any way to pass a value to the map and have that marker load only?

    Reply
  2. Jon Post author

    Hi Quinten, no problem. To make the points load automatically you just need to move the innards of the addCitiesToMap function into viewDidLoad (Code that needs moving below)

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    [self downloadAndParseJSONCities:^(NSArray *annotations) {

    [self.mapView addAnnotations:annotations];

    } :^(NSError *error) {

    NSLog(@”There was an error downloading annotations from the server %@”,[error localizedDescription]);

    }];

    });

    I am not sure I follow your second question, do you mean how do you retrieve just a single capital city?

    Reply
    1. Quinten

      Hi Jon,

      Thanks for your help.

      I wanted to know if I had the city names listed in a table view controller how do I pass an individual city to a map so that only that city shows with the pin and annotations.

      Quinten

      Reply
  3. Quinten

    Hi Jon, I am having some problems implementing as I am using a sqlite database and not a json file. Do you do paid freelance work. What i want is actually pretty simple with the maps. Let me know if you can help and cost.

    Reply
    1. Jon Post author

      Hi Quinten,

      Sure, if you send me a mail jon@jonathanfield.me with what you need I will take a look at it for you, no charge but if you would to make that a small donation towards hosting costs then they are always welcome :)

      Reply
  4. Chris

    Hey Jon,
    thanks for the great tutorial! But how can I get a location, for example “new york” shown on the native map app in ios? is it possible to show more information in an annotation, for example the whole adress?

    Reply
  5. Chris

    Hey Jon,
    thanks for the link, but I dont want to reverse the geocode. I just want the Location of the Annotation from the JSON File shown up in the iOS App. Like a delivery from the JSON File in the App to the native Maps App in iOS.

    Reply
    1. Jon Post author

      Hi Chris,

      Do you mean that you want to show the Apple Maps with a location that you have clicked inside another App?

      Reply
      1. Chris

        Hey Jon,

        yes. Sorry for my bad english, i know that its hard to understand. I just want that Apple Maps opens up, when I click on an Annotation inside of your Map App. I hope you understand me :/

        Reply
        1. Jon Post author

          Hi Chris,

          You need to use the Apple Maps Custom URL scheme to launch the maps app. From wherever you want the action to be trigged (Tableview delegate didSelectRow ?) you need to call the following.

          [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://maps.apple.com/?ll=%f,%f",PUT_YOUR_LATITUDE_HERE,PUT_YOUR_LONGITUDE_HERE]]];

          There is more information on the Custom URL Scheme here, https://developer.apple.com/library/ios/featuredarticles/iPhoneURLScheme_Reference/MapLinks/MapLinks.html

          Reply
  6. Shabna

    Hi Jon, awesome tutorial! I wanted to make the maps display custom points but when I change the values in the json file, nothing comes up. How could I do this? For example I just wanted to have points in Greenwich, London and I’d want the map to be just of that area.

    Any help would be appreciated! :)

    Reply
    1. Jon Post author

      Hi, thanks.

      Can you give me an example of the JSON file that you are working with?

      JF

      Reply
  7. Dax

    Het Jon,

    Thanks for the great tutorial!! This really helped me a lot!
    But, in my case I need to use a Segmented Control to show of different types of annotations.
    Is there a way to do that, because I can’t fix it :( ? Or am I asking a stupid question?

    Dax

    Reply
    1. Jon Post author

      Hi Dax,

      What exactly are you looking for the segmented control to do?

      JF

      Reply
      1. Dax

        Dear Jon,

        I want a segmented control with 3 segments.
        On all the 3 segments I need to show different location annotations.

        Example:
        So when I do segment 1, named ‘x’, it shows 6 different locations.
        And when I do segment 2, named ‘y’, it removes the segment locations from segment 1 and shows (for example) other 8 location annotations.
        Some thing for segment 3 of course.

        Hope this explains my problem :)

        Dax

        Reply
        1. Jon Post author

          Hi Dax,

          That is quite straight forward.

          You will need to maintain a list of those annotations in a data structure such as an NSArray. You then need to mark them to correspond to the specific section that has been selected, perhaps you could use the .tag attribute or add your own attribute that can be mapped to a constant.

          Then on the value changed function triggered by selecting a segment inside the segmented control, you need to update the MapView to show the new set of annotations, potentially removing the previous ones from the map.

          Does that make sense?

          I might be able to put together an example project for you after the weekend if you are still having trouble.

          JF

          Reply
          1. Dax

            I makes a lot of sense Jon! Thanks!
            Can I make a NSMutable Array the same way as this:
            _________________________________________

            @interface PhotosTableViewController () {
            NSMutableArray *photos;
            }
            @property (weak, nonatomic) IBOutlet UILabel *AfvalLabel;

            @end

            @implementation PhotosTableViewController

            - (void)viewDidLoad
            {
            [super viewDidLoad];

            photos = [[NSMutableArray alloc]init];

            Photo *pic = [[Photo alloc]init];
            pic.name = @”Papier en karton”;
            pic.filename = @”papier”;
            pic.notes = @”Oude kranten, tijdschriften en karton (zoals platgeslagen dozen) doet u in de papiercontainer. Zet géén dozen bij het grofvuil, maar breng ze naar een afvalpunt of maak de dozen klein en doe ze in de papiercontainer.”;
            [photos addObject:pic];

            pic = [[Photo alloc]init];
            pic.name = @”Glas”;
            pic.filename = @”glas”;
            pic.notes = @”Gooi nooit zo maar glas weg, want bij het maken van nieuw glas wordt minimaal 50% oud glas gebruikt. Gebruikte flessen, potten en ander kapot glaswerk brengt u naar de glasbak. Ruiten, spiegels en ander vlakglas brengt u naar een afvalpunt. Laat dit niet bij de glasbak achter. \n\nGlas in de zak veroorzaakt ongevallen. Help ons deze te voorkomen. Doe glas in de bak!”;
            [photos addObject:pic];

            _________________________________________

            I’m just a beginner with Objective-C and xcode. It’s one big adventure.

            I would really appreciate it if you could help me.
            Let you know how the progress goes.

            Greeting,

            Dax

Leave a Reply to Quinten Cancel reply