Introduction to MapKit in iOS 6 Tutorial

A tutorial that shows you how you can use MapKit in your iOS apps to show maps, drop pins, look up addresses, and more! By Matt Galloway.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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 the Maps app. They don’t necessarily have to be pins – we are 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 an instance of MKAnnotationView to present as a visual indicator of the annotation. We’ll be using the base class, MKAnnotationView, in this tutorial but there is a concrete subclass called MKPinAnnotationView which can be used if you want the standard pin you see in the Maps app.

Ok, so let’s start with step 1. Select your ArrestsPlotter group, go to File\New\New File, choose iOS\Cocoa Touch\Objective-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:

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

@interface MyLocation : NSObject <MKAnnotation>

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

@end

This is a plain old NSObject with a special initializer and a method to return an MKMapItem (more on that in a minute). 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:

#import "MyLocation.h"
#import <AddressBook/AddressBook.h>

@interface MyLocation ()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, assign) CLLocationCoordinate2D theCoordinate;
@end

@implementation MyLocation

- (id)initWithName:(NSString*)name address:(NSString*)address coordinate:(CLLocationCoordinate2D)coordinate {
    if ((self = [super init])) {
        if ([name isKindOfClass:[NSString class]]) {
            self.name = name;
        } else {
            self.name = @"Unknown charge";
        }
        self.address = address;
        self.theCoordinate = coordinate;
    }
    return self;
}

- (NSString *)title {
    return _name;
}

- (NSString *)subtitle {
    return _address;
}

- (CLLocationCoordinate2D)coordinate {
    return _theCoordinate;
}

- (MKMapItem*)mapItem {
    NSDictionary *addressDict = @{(NSString*)kABPersonAddressStreetKey : _address};
    
    MKPlacemark *placemark = [[MKPlacemark alloc]
                              initWithCoordinate:self.coordinate
                              addressDictionary:addressDict];
    
    MKMapItem *mapItem = [[MKMapItem alloc] initWithPlacemark:placemark];
    mapItem.name = self.title;
    
    return mapItem;
}

@end

Again a simple implementation here – note the required title method (from MKAnnotation protocol) returns the name, and the required subtitle method returns the address. The mapItem method is more interesting. This is creating an instance of a special class called MKMapItem to represent this location. What this class does is provide a way for you to pass information to the Maps app. You’re going to add the ability in a minute to open the Maps app directly from within your app! It’ll show the location in the standard Maps app. Pretty neat!

One more thing you need to do right now is to add the AddressBook framework. Do this like you did before by selecting the ArrestsPlotter project on the left panel of Xcode. Then select the Build Phases tab within the ArrestsPlotter target, open Link Binary with Libraries panel and click the plus button. Finally select AddressBook.framework and select Add. You need this because you’re using kABPersonAddressStreetKey from that framework in the mapItem method.

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:

// Add to top of file
#import "MyLocation.h"

// Add new method above refreshTapped
- (void)plotCrimePositions:(NSData *)responseData {
    for (id<MKAnnotation> annotation in _mapView.annotations) {
        [_mapView removeAnnotation:annotation];
    }
    
    NSDictionary *root = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil];
    NSArray *data = [root objectForKey:@"data"];
    
    for (NSArray *row in data) {
        NSNumber * latitude = [[row objectAtIndex:22]objectAtIndex:1];
        NSNumber * longitude = [[row objectAtIndex:22]objectAtIndex:2];
        NSString * crimeDescription = [row objectAtIndex:18];
        NSString * address = [row objectAtIndex:14];
        
        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:request.responseData];

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. Then set the View Controller (right at the top of the list) 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:

@interface ViewController : UIViewController <MKMapViewDelegate>

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

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
    static NSString *identifier = @"MyLocation";   
    if ([annotation isKindOfClass:[MyLocation class]]) {
        
        MKAnnotationView *annotationView = (MKAnnotationView *) [_mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
        if (annotationView == nil) {
            annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
            annotationView.enabled = YES;
            annotationView.canShowCallout = YES;
            annotationView.image = [UIImage imageNamed:@"arrest.png"];//here we use a nice image instead of the default pins
        } else {
            annotationView.annotation = annotation;
        }
        
        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 the plain vanilla MKAnnotationView. It uses the title and subtitle of our MyLocation class to determine what to show in the callout (the little bubble that pops up when you tap on it). Note we customized the annotation so that it shows an image, 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!

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