MapKit is a really neat API available on the iPhone that makes it easy to display maps, jump to coordinates, plot locations, and even draw routes and other shapes on top.

I’m writing this tutorial because about a month ago I attended a neat event in Baltimore called Civic Hack Day where I played around with MapKit to display some public data such as place locations, crime data, arrests and bus routes. It was kind of fun so thought others might be interested to learn how it works – and maybe use a similar technique with data from your own hometown!

In this tutorial, we’re going to make an app that zoomes into aparticularly cool location in Baltimore. The app will then query a Baltimore web service to pull back the recent arrests around that area, and plot them on the map.

In the process, you’ll learn how to add a MapKit map to your app, zoom to a particular location, query and retrieve government data available via the Socrata API, create custom map annotations, and more!

This tutorial uses Xcode 4.2 with iOS 5 features such as Storyboard, ARC. So we can use the latest and greatest available to us!

Without further ado, let’s get mapping!

Getting Started

In Xcode 4.2, go to FileNewNew Project, select iOSApplicationSingleViewApplication, and click Next. Then type ArrestsPlotter as the project name. Make sure Use Storyboard and Use Automatic Reference Counting options are checked, click Next, and choose a directory to save your project in to finish.

Click on MainStoryboard.storyboard to bring up Interface Builder. This is one of the fabulous feature introduced with the iOS 5, all interfaces are manageable within one single file!

Bring up the Object library by selecting the third tab in the View toolbar (Utilities) and selecting the third tab in the library toolbar (Object library), as shown in the screenshot below.

Object Library in Xcode 4.2

From the Object library, drag a toolbar to the bottom of the screen, and a Map View to the middle of the screen, and rename the toolbar button to read “Refresh”, as shown in the screenshot below.

View Layout with MapKit

Next, click on the map view you added to the middle of the screen, and click on the fourth tab in the inspector toolbar to switch to the Attributes inspector. Click the checkbox for Shows User Location, as shown below.

Show User Location on Map View

Almost done, but before you can run your code you need to add the MapKit framework to your project (or else it will crash on startup!)

To do this in Xcode 4.2, click on the name of your project in the Groups & Files tree, select the ArrestsPlotter target, and switch to the Build Phases tab. Under the Link Binary With Libraries section, click the Plus button, find the entry for MapKit.framework, and click Add.

At this point your screen should look like the screenshot below:

Adding a Framework with Xcode 4

Now you should be able to compile and run your project, and have a fully zoomable-and-pannable map showing your current location (or Cupertino if you’re using the Simulator), using Google Maps!

Default MapKit Map

So far so good, eh? But we don’t want the map to start looking at the entire world – we want to take a look at a particular area!

Setting Visible Area

Start by switching to ViewController.h, and replace it with the following:

  1. #import <UIKit/UIKit.h>
  2. #import <MapKit/MapKit.h>
  3.  
  4. #define METERS_PER_MILE 1609.344
  5.  
  6. @interface ViewController : UIViewController{
  7. }
  8.  
  9. @end
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

#define METERS_PER_MILE 1609.344

@interface ViewController : UIViewController{
}

@end

This simply adds a constant for meters per mile and adds an instance variable we’ll use to keep track of whether we’ve already zoomed in or not.

Next, we need to connect the Map View you created in Interface Builder to an instance variable in your view controller. We could do this the old fashioned way (create an instance variable and property, and connect with Interface Builder by right clicking and drawing lines from the outlet to the map view), but there’s an even easier way in Xcode 4.2!

To do this, select MainStoryboard.storyboard again, and make sure that the assistant editor is selected (the second tab in the Editor tab group). I like to have mine show up at the bottom – you can set the position with ViewAssistant LayoutAssistant Editors at Bottom.

In the toolbar at the top of the Assistant Editor, make sure Automatic is selected, and that it is set to display ViewController.h. If it’s not, click on Automatic, on the drop down list, choose Manual and look for ViewController.h like shown in the screenshot below.

Show File With Assistant Editor

Now, control-drag from the Map View down to your header file, right below where the curly brace ends the @interface declaration, and before the @end.

A popup will appear. Set the connection type to Outlet, the name to _mapView, keep the Type as MKMapView, and click Connect. It will automatically make a property for your map view and hook it up for you!

Automatic Outlet Creation with Xcode 4.2

This calls for a celebration – 3 w00ts (and one rage) for Xcode 4!

Xcode 4 Rage Comic

Ahem – back to work! Switch to ViewController.m and implement viewWillAppear to zoom in to an initial location on startup:

  1. - (void)viewWillAppear:(BOOL)animated {
  2.    // 1
  3.     CLLocationCoordinate2D zoomLocation;
  4.     zoomLocation.latitude = 39.281516;
  5.     zoomLocation.longitude= -76.580806;
  6.      // 2
  7.     MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(zoomLocation, 0.5*METERS_PER_MILE, 0.5*METERS_PER_MILE);
  8.     // 3
  9.     MKCoordinateRegion adjustedRegion = [_mapView regionThatFits:viewRegion];
  10.     // 4
  11.     [_mapView setRegion:adjustedRegion animated:YES];
  12. }
- (void)viewWillAppear:(BOOL)animated {
   // 1
    CLLocationCoordinate2D zoomLocation;
    zoomLocation.latitude = 39.281516;
    zoomLocation.longitude= -76.580806;
     // 2
    MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(zoomLocation, 0.5*METERS_PER_MILE, 0.5*METERS_PER_MILE);
    // 3
    MKCoordinateRegion adjustedRegion = [_mapView regionThatFits:viewRegion];
    // 4
    [_mapView setRegion:adjustedRegion animated:YES];
}

There’s a lot of new stuff here, so let’s go over it bit by bit.

  1. Picks out the location to zoom in. Here we choose the location in Baltimore where I initially wrote this app, which is a good choice for the BPD Arrests API we’ll be using later on in this tutorial.
  2. When you are trying to tell the map what to display, you can’t just give a lat/long – you need to specify the box (or region) to display. So this uses a helper function to make a region around a center point (the user’s location) and a given width/height. We use half a mile here, because that works well for plotting arrests data.
  3. Before sending the region on to the map view, you have to trim the region a bit into what can actually fit on the screen, given the current size of the map view’s frame. The map view has a helper method called regionThatFits that does exactly that.
  4. Finally, tells the mapView to display the region. The map view automatically transitions the current view to the desired region with a neat zoom animation with no extra code required!

Compile and run the app, and now it should zoom in to Baltimore area :]

Zooming into a location with MapKit

Obtaining Arrests Data: The Plan

The next step is to plot some interesting arrests data around our current location. But where in the heck can we get such stuff??

Well, it depends on your current location. Here in Baltimore, we are quite lucky because the city is working quite hard to making all city data available online, through the OpenBaltimore initiative.

We will be using this dataset for the purposes of this tutorial. After you finish this tutorial, maybe look around to see if your city has an alternate dataset you can use?

Anyway, the Baltimore city data is made available through a company named Socrata, who has an API you can use to access the data. The Socrata API documentation is available online, so we aren’t going to go into the gory details here, except to explain the high level plan of attack:

  1. The specific dataset we’re interested in is the BPD Arrests. Using this link, you can take a peek at the raw data, and if you click ExportAPI, you can see the API access endpoint we’ll be using.
  2. To query the API, you basically issue a POST to the given Socrata URL, and pass in a query as JSON. The results will come back as JSON as well. You can learn more about the command and response formats in the Socrata API documentation, but you don’t really need to know the details for this tutorial.
  3. The query we need to use is on the largish end, so we’ll store it in a text file to make it a bit easier to read and edit, and do some subtitutions in the code.
  4. To save time, we’ll use ASIHTTPRequest to assist with sending/receiving data to the web service, and the SBJSON Framework library (Previously known as json-framework) to parse the JSON data.

Ok – so we’ve got a plan, but before we can begin, we need to quickly add the ASIHTTPRequest and SBJSON libraries to our project.

Adding the Libraries

To add the SBJSON framework, first download it from its github page, make sure to download the latest release which supports ARC. Once you have it downloaded, right click your ArrestsPlotter project entry in groups and files, select New Group, and name the new group SBJSON. Then drag all of the files from the SBJSONClasses directory (SBJSON.h and several others) into the new SBJSON group. Make sure “Copy items into destination group’s folder (if needed)” is selected, and click Finish.

To add ASIHTTPRequest, first download it. Once you have it downloaded, right click your ArrestsPlotter project entry in groups and files, select New Group, and name the new group ASIHTTPRequest. Then drag all of the files from the ASIHTTPRequestClasses directory (ASIAuthenticationDialog.h and several others, but don’t add in the folder) into the new ASIHTTPRequest group. Make sure “Copy items into destination group’s folder (if needed)” is selected, and click Finish.

Also repeat this for the two files (Reachability.h and Reachability.m) in ASIHTTPRequestExternalReachability, as these are dependencies of the project.

To add MBProgressHUD, first download it. Once you have it downloaded, right click your ArrestsPlotter project entry in groups and files, select New Group, and name the new group MBProgressHUD. Then drag MBProgressHUD.h and MBProgressHUD.m into the new MBProgressHUD group. Make sure “Copy items into destination group’s folder (if needed)” is selected, and click Finish.

The last step is you need to link your project against a few required frameworks. To do this, click on your ArrestsPlotter project entry in Groups & Files, click the ArrestsPlotter target, and choose the Build Phases tab. Click the plus button, and choose CFNetwork.framework. Then repeat this for SystemConfiguration.framework, MobileCoreServices.framework, and libz.1.1.3.dylib.

Framework dependencies in Xcode 4.2

Note that, so far, if you compile the app you will run several error messages like – autorelease is unavailable, retain is unavailable and ARC forbids explicit message send of ‘release’ and many others around the ARC feature.

Actually, projects with Automatic Reference Counting (ARC) enabled can use ASIHTTPRequest. However, since ASIHTTPRequest’s codebase does not use ARC, you will need to add compiler flags to get everything working. This is pretty easy. In Xcode, go to your active target and select the “Build Phases” tab. In the “Compiler Flags” column, set -fno-objc-arc for each of the ASIHTTPRequest source files.

Adapt ASIHTTPRequest with ARC

Compile your project just to make sure you’re good so far, and now we’re back to the fun stuff!

Obtaining Arrests Data: The Implementation

First, download this resource file which contains a template for the query string we need to send to the Socrata API web service to get the arrests near a particular location. When you get the file, unzip it and drag command.json into your ArrestsPlotterSupporting Files group, make sure “Copy items into destination group’s folder (if needed)” is selected, and click Finish.

Next, you need to set up the “Refresh” button on the toolbar to call a method, so you know when it’s tapped and can search for the arrests data around the current location. Again, you could do this the old way (make an IBAction outlet and connect with Interface Builder), but let’s try the stylish new Xcode 4.2 way!

To do this, click MainStoryboard.storyboard, select the Refresh button, and control drag from the button to ViewController.h, to the line right after the mapView outlet. Change the Connection type to Action, the Name to refreshTapped, keep the Type as id, and click Connect. Xcode will automatically create the method for you in both the header and implementation, and connect it too!

Then switch over to ViewController.m and make the following changes:

  1. // At top of file
  2. #import "ASIHTTPRequest.h"
  3.  
  4. // Replace refreshTapped as follows
  5. - (IBAction)refreshTapped:(id)sender {
  6.  
  7.      // 1
  8.     MKCoordinateRegion mapRegion = [_mapView region];
  9.     CLLocationCoordinate2D centerLocation = mapRegion.center;
  10.  
  11.     // 2
  12.     NSString *jsonFile = [[NSBundle mainBundle] pathForResource:@"command" ofType:@"json"];
  13.     NSString *formatString = [NSString stringWithContentsOfFile:jsonFile encoding:NSUTF8StringEncoding error:nil];
  14.     NSString *json = [NSString stringWithFormat:formatString,
  15.                       centerLocation.latitude, centerLocation.longitude, 0.5*METERS_PER_MILE];
  16.  
  17.     // 3
  18.     NSURL *url = [NSURL URLWithString:@"http://data.baltimorecity.gov/api/views/INLINE/rows.json?method=index"];
  19.  
  20.     // 4
  21.     ASIHTTPRequest *_request = [ASIHTTPRequest requestWithURL:url];
  22.     __weak ASIHTTPRequest *request = _request;
  23.  
  24.     request.requestMethod = @"POST";
  25.     [request addRequestHeader:@"Content-Type" value:@"application/json"];
  26.     [request appendPostData:[json dataUsingEncoding:NSUTF8StringEncoding]];
  27.     // 5
  28.     [request setDelegate:self];
  29.     [request setCompletionBlock:^{
  30.         NSString *responseString = [request responseString];
  31.         NSLog(@"Response: %@", responseString);
  32.     }];
  33.     [request setFailedBlock:^{
  34.         NSError *error = [request error];
  35.         NSLog(@"Error: %@", error.localizedDescription);
  36.     }];
  37.  
  38.     // 6
  39.     [request startAsynchronous];
  40.  
  41. }
// At top of file
#import "ASIHTTPRequest.h"

// Replace refreshTapped as follows
- (IBAction)refreshTapped:(id)sender {

     // 1
    MKCoordinateRegion mapRegion = [_mapView region];
    CLLocationCoordinate2D centerLocation = mapRegion.center;

    // 2
    NSString *jsonFile = [[NSBundle mainBundle] pathForResource:@"command" ofType:@"json"];
    NSString *formatString = [NSString stringWithContentsOfFile:jsonFile encoding:NSUTF8StringEncoding error:nil];
    NSString *json = [NSString stringWithFormat:formatString,
                      centerLocation.latitude, centerLocation.longitude, 0.5*METERS_PER_MILE];

    // 3
    NSURL *url = [NSURL URLWithString:@"http://data.baltimorecity.gov/api/views/INLINE/rows.json?method=index"];

    // 4
    ASIHTTPRequest *_request = [ASIHTTPRequest requestWithURL:url];
    __weak ASIHTTPRequest *request = _request;

    request.requestMethod = @"POST";
    [request addRequestHeader:@"Content-Type" value:@"application/json"];
    [request appendPostData:[json dataUsingEncoding:NSUTF8StringEncoding]];
    // 5
    [request setDelegate:self];
    [request setCompletionBlock:^{
        NSString *responseString = [request responseString];
        NSLog(@"Response: %@", responseString);
    }];
    [request setFailedBlock:^{
        NSError *error = [request error];
        NSLog(@"Error: %@", error.localizedDescription);
    }];

    // 6
    [request startAsynchronous];

}

Let’s review this section by section.

  1. Gets the lat/long for the center of the map.
  2. Reads in the command file template that you downloaded from this site, which is the query string you need to send to the Socrata API to get the arrests within a radius of a particular location. It also has a hardcoded date restriction in there to keep the data set managable. The command file is set up to be a query string, so you can substitute the lat/long and radius in there as parameters. It has a hardcoded radius here (0.5 miles) to again keep the returned data managable.
  3. Creates a URL for the web service endpoint to query.
  4. Creates a ASIHTTPRequest request, and sets it up as a POST, passing in the JSON string as data.
  5. Sets up two blocks for the completion and failure. So far on this site we’ve been using callback methods (instead of blocks) with ASIHTTPRequest, but I wanted to show you the block method here because it’s kinda cool and convenient. Right now, these do nothing but log the results.
  6. Finally, starts the request going asynchronously. When it completes, either the completion or error block will be executed.

Compile and run your code, and if all works well you should see some data in your console when you click refresh, similar to the following:

Arrests Data Web Service Results

Plotting the Data

Ok so now that we have interesting data points to show, all we have to do is add them to the map!

With MapKit, you can do this with something called a “map annotation”. Think of map annotations as the little pins that show up in Google Maps (except they don’t necessarily have to be pins, this is said, we gonna make them look something different!)

To use annotations there are three steps:

  1. Create a class that implements the MKAnnotation protocol. This means it needs to return a title, subtitle, and coordinate. You can store other information on there if you want too.
  2. For every location you want marked on the map, create one of these classes and add it to the mapView with the addAnnotation method.
  3. Mark the view controller as the map view’s delegate, and for each annotation you added it will call a method on the view controller called mapView:viewForAnnotation. Your job in this method is to return a subclass of MKAnnotationView to present as a visual indicator of the annotation. There’s a built-in one called MKPinAnnotationView that we’ll be using in this tutorial.

Ok, so let’s start with step 1. Select your ArrestsPlotter group, go to FileNewNew File, choose iOSCocoa TouchObjective-C class, and click Next. Enter MyLocation for the class, make it a subclass of NSObject, and finish creating the file.

Replace MyLocation.h with the following:

  1. #import <Foundation/Foundation.h>
  2. #import <MapKit/MapKit.h>
  3.  
  4. @interface MyLocation : NSObject <MKAnnotation> {
  5.     NSString *_name;
  6.     NSString *_address;
  7.     CLLocationCoordinate2D _coordinate;
  8. }
  9.  
  10. @property (copy) NSString *name;
  11. @property (copy) NSString *address;
  12. @property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
  13.  
  14. - (id)initWithName:(NSString*)name address:(NSString*)address coordinate:(CLLocationCoordinate2D)coordinate;
  15.  
  16. @end
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface MyLocation : NSObject <MKAnnotation> {
    NSString *_name;
    NSString *_address;
    CLLocationCoordinate2D _coordinate;
}

@property (copy) NSString *name;
@property (copy) NSString *address;
@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;

- (id)initWithName:(NSString*)name address:(NSString*)address coordinate:(CLLocationCoordinate2D)coordinate;

@end

This is a plain old NSObject with a few instance vars/properties and an initializer. Note it marks itself as implementing the MKAnnotation protocol. This means that the coordinate property is required, and so are the title and subtitle methods (which we’ll be adding next).

Now replace MyLocation.m with the following:

  1. #import "MyLocation.h"
  2.  
  3. @implementation MyLocation
  4. @synthesize name = _name;
  5. @synthesize address = _address;
  6. @synthesize coordinate = _coordinate;
  7.  
  8. - (id)initWithName:(NSString*)name address:(NSString*)address coordinate:(CLLocationCoordinate2D)coordinate {
  9.     if ((self = [super init])) {
  10.         _name = [name copy];
  11.         _address = [address copy];
  12.         _coordinate = coordinate;
  13.     }
  14.     return self;
  15. }
  16.  
  17. - (NSString *)title {
  18.     if ([_name isKindOfClass:[NSNull class]])
  19.         return @"Unknown charge";
  20.     else
  21.         return _name;
  22. }
  23.  
  24. - (NSString *)subtitle {
  25.     return _address;
  26. }
  27.  
  28. @end
#import "MyLocation.h"

@implementation MyLocation
@synthesize name = _name;
@synthesize address = _address;
@synthesize coordinate = _coordinate;

- (id)initWithName:(NSString*)name address:(NSString*)address coordinate:(CLLocationCoordinate2D)coordinate {
    if ((self = [super init])) {
        _name = [name copy];
        _address = [address copy];
        _coordinate = coordinate;
    }
    return self;
}

- (NSString *)title {
    if ([_name isKindOfClass:[NSNull class]])
        return @"Unknown charge";
    else
        return _name;
}

- (NSString *)subtitle {
    return _address;
}

@end

Again a simple implementation here – note the required title method returns the name, and the required subtitle method returns the address.

Onto step 2 – add an instance of one of these classes for every arrest we wish to plot. Make the following changes to ViewController.m:

  1. // Add to top of file
  2. #import "MyLocation.h"
  3. #import "SBJSON.h"
  4.  
  5. // Add new method above refreshTapped
  6. - (void)plotCrimePositions:(NSString *)responseString {
  7.  
  8.     for (id<MKAnnotation> annotation in _mapView.annotations) {
  9.         [_mapView removeAnnotation:annotation];
  10.     }
  11.  
  12.     NSDictionary * root = [responseString JSONValue];
  13.     NSArray *data = [root objectForKey:@"data"];
  14.  
  15.     for (NSArray * row in data) {
  16.  
  17.         NSNumber * latitude = [[row objectAtIndex:21]objectAtIndex:1];
  18.         NSNumber * longitude = [[row objectAtIndex:21]objectAtIndex:2];
  19.         NSString * crimeDescription =[row objectAtIndex:17];
  20.         NSString * address = [row objectAtIndex:13];
  21.  
  22.         CLLocationCoordinate2D coordinate;
  23.         coordinate.latitude = latitude.doubleValue;
  24.         coordinate.longitude = longitude.doubleValue;
  25.         MyLocation *annotation = [[MyLocation alloc] initWithName:crimeDescription address:address coordinate:coordinate] ;
  26.         [_mapView addAnnotation:annotation];
  27.          }
  28. }
  29.  
  30. // Add new line inside refreshTapped, in the setCompletionBlock, right after logging the response string
  31. [self plotCrimePositions:responseString];
// Add to top of file
#import "MyLocation.h"
#import "SBJSON.h"

// Add new method above refreshTapped
- (void)plotCrimePositions:(NSString *)responseString {

    for (id<MKAnnotation> annotation in _mapView.annotations) {
        [_mapView removeAnnotation:annotation];
    }

    NSDictionary * root = [responseString JSONValue];
    NSArray *data = [root objectForKey:@"data"];

    for (NSArray * row in data) {

        NSNumber * latitude = [[row objectAtIndex:21]objectAtIndex:1];
        NSNumber * longitude = [[row objectAtIndex:21]objectAtIndex:2];
        NSString * crimeDescription =[row objectAtIndex:17];
        NSString * address = [row objectAtIndex:13];

        CLLocationCoordinate2D coordinate;
        coordinate.latitude = latitude.doubleValue;
        coordinate.longitude = longitude.doubleValue;
        MyLocation *annotation = [[MyLocation alloc] initWithName:crimeDescription address:address coordinate:coordinate] ;
        [_mapView addAnnotation:annotation];
         }
}

// Add new line inside refreshTapped, in the setCompletionBlock, right after logging the response string
[self plotCrimePositions:responseString];

The important code here is inside the new method, plotCrimePositions. It first removes any annotations already on the map so you start with a clean slate. Then it parses the response string as JSON, and pulls out the interesting info from it (lat/long, arrest description, etc), given the hardcoded offsets at which this data appears in the JSON results.

Once it’s pulled out the interesting data, it creates a new MyLocation object and adds it as an annotation to the map view.

Ok, onto the third and final step! First select MainStoryboard.storyboard, and control click on the Map View. Set the File’s Owner as the delegate of the map view by control clicking on the map view, and dragging a line from the delegate entry to the View Controller.

Then go to ViewController.h, and mark the class as implementing MKMapViewDelegate as follows:

  1. @interface ViewController : UIViewController <MKMapViewDelegate> {
@interface ViewController : UIViewController <MKMapViewDelegate> {

Then add a new method to ViewController.m as follows:

  1. - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
  2.  
  3.     static NSString *identifier = @"MyLocation";
  4.     if ([annotation isKindOfClass:[MyLocation class]]) {
  5.  
  6.         MKPinAnnotationView *annotationView = (MKPinAnnotationView *) [_mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
  7.         if (annotationView == nil) {
  8.             annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
  9.         } else {
  10.             annotationView.annotation = annotation;
  11.         }
  12.  
  13.         annotationView.enabled = YES;
  14.         annotationView.canShowCallout = YES;
  15.         annotationView.image=[UIImage imageNamed:@"arrest.png"];//here we use a nice image instead of the default pins
  16.  
  17.         return annotationView;
  18.     }
  19.  
  20.     return nil;
  21. }
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {

    static NSString *identifier = @"MyLocation";
    if ([annotation isKindOfClass:[MyLocation class]]) {

        MKPinAnnotationView *annotationView = (MKPinAnnotationView *) [_mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
        if (annotationView == nil) {
            annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
        } else {
            annotationView.annotation = annotation;
        }

        annotationView.enabled = YES;
        annotationView.canShowCallout = YES;
        annotationView.image=[UIImage imageNamed:@"arrest.png"];//here we use a nice image instead of the default pins

        return annotationView;
    }

    return nil;
}

This is the method that gets called for every annotation you added to the map (kind of like tableView:cellForRowAtIndexPath), that needs to return the view for each annotation.

Also similarly to tableView:cellForRowAtIndexPath, map views are set up to reuse annotation views when some are no longer visible. So the code first checks to see if a reusable annotation view is available before creating a new one.

Update: One extra thing to point out about this, suggested by Kalgar (thanks man!) Note that when you dequeue a reusable annotation, you give it an identifier. If you have multiple styles of annotations, be sure to have a unique identifier for each one, otherwise you might mistakenly dequeue an identifier of a different type, and have unexpected behavior in your app. It’s basically the same idea behind a cell identifier in tableView:cellForRowAtIndexPath.

Here we use built-in MKAnnotationView subclass called MKPinAnnotationView. It uses the title and subtitle of our MyLocation class to determine what to show in the callout. Note we customized the annotation so that it shows an image instead, for fun.

Speaking of an image, be sure to download this custom image and add it to your project.

And that’s it! Compile and run your code, and now you should be able to zoom around Baltimore searching for arrest!

Baltimore Crime Map

(Hint: in the land of the Wire, you won’t have to search very far! :])

Adding a Progress indicator

The App works fine so far, but it’d be better if we let the user know what’s going on when we pass the HTTP Request, a nice and easy MBProgressHUD will do that so let’s quickly add it in the ViewController.m:

  1. // Add at the top of the file
  2. #import "MBProgressHUD.h"
  3.  
  4. // Add right after [request startAsynchronous] in refreshTapped action method
  5. MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
  6. hud.labelText = @"Loading arrests...";
  7.  
  8. // Add at start of setCompletionBlock and setFailedBlock blocks
  9. [MBProgressHUD hideHUDForView:self.view animated:YES];
// Add at the top of the file
#import "MBProgressHUD.h"

// Add right after [request startAsynchronous] in refreshTapped action method
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.labelText = @"Loading arrests...";

// Add at start of setCompletionBlock and setFailedBlock blocks
[MBProgressHUD hideHUDForView:self.view animated:YES];

Ahh… much more responsive! :]

Where To Go From Here?

Here is a sample project with all of the code we’ve developed in the above tutorial.