Saturday 6 October 2012

Introduction to MapKit in iOS 6


This Post uses Xcode 4.5 with iOS 6 features such as Objective-C literals and the new mapping engine developed directly by Apple. It also uses iOS 5 features such as Storyboard, ARC. You really are living at the cutting edge with us!
Without further ado, let’s get mapping!

Getting Started

In Xcode 4.5, go to File\New\New Project, select iOS\Application\Single View Application, and click Next. Then type ArrestsPlotter as the project name. Make sure Use Storyboard and Use Automatic Reference Counting options are checked. Also make sure iPhone is selected for the Devices option. Then click Next, and choose a directory to save your project in to finish.
Click on MainStoryboard.storyboard to bring up Interface Builder. Then 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. It’s important to add the toolbar first and then the map, because if you do it that way round you’ll notice that the map automagically takes up the remaining space. It’s as if Xcode is reading your mind!
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.5, click on the name of your project in the Groups & Files tree, select the ArrestsPlotter target, and switch to the Build Phases tab. Open 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!
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!
As an aside, you might want to at this point experiment with Xcode’s simulate location feature. You’ll notice that the app currently thinks you’re in Cupertino. That’s all well and good, but maybe you want to pretend you’re somewhere else far far away from 1 Infinite Loop. Although I’m not sure why you wouldn’t want to pretend you were there… Anyway, you can simulate your location by clicking the location indicator icon at the bottom of the main Xcode panel whilst the app is running. From there you can select a location. See the screenshot below for an example.

Setting Visible Area

First, 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!
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 View\Assistant Layout\Assistant 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 forViewController.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 in the middle of 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, the Storage as Weak, and click Connect. It will automatically make a property for your map view and hook it up for you!
This calls for a celebration – 3 w00ts (and one rage) for Xcode 4!
Xcode 4 Rage Comic
(If you don’t get this, check out the Introduction to In-App Purchases tutorial for more details on rage comics :])
Ahem – back to work! Open ViewController.m, and add the following underneath the #imports and before the @implementation:
#define METERS_PER_MILE 1609.344
This simply adds a constant for meters per mile which you’ll be using later. Now implement viewWillAppear to zoom in to an initial location on startup:
- (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
    [_mapView setRegion:viewRegion 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. 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 :]

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 Export\API, 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.
Ok – so we’ve got a plan, but before we can begin, we need to quickly add the ASIHTTPRequest library to our project.

Adding the Libraries

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 (but not the folders) from within the ASIHTTPRequest\Classes directory (ASIAuthenticationDialog.h and several others) into the new ASIHTTPRequest group. Make sure “Copy items into destination group’s folder (if needed)” and “Add to targets -> ArrestsPlotter” are selected, and click Finish.
Also repeat this for the two files (Reachability.h and Reachability.m) in ASIHTTPRequest\External\Reachability, 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)” and “Add to targets -> ArrestsPlotter” are 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.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 (including Reachability.m).
Compile your project just to make sure you’re good so far, and now we’re back to the fun stuff! You will notice a few warnings coming from ASIHTTPRequest and MBProgressHUD which you can simply ignore. They are just there because it hasn’t been fully updated yet to support the new iOS 6 SDK.

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 ArrestsPlotter\Supporting Files group, make sure “Copy items into destination group’s folder (if needed)” and “Add to targets -> ArrestsPlotter” are 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 you might as well use the new super-duper, automagic 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:
// 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 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 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 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[21][1];
        NSNumber *longitude = row[21][2];
        NSString *crimeDescription = row[17];
        NSString *address = row[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: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]]) {
 
        MKPinAnnotationView *annotationView = (MKPinAnnotationView *) [_mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
        if (annotationView == nil) {
            annotationView = [[MKPinAnnotationView 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 built-in MKAnnotationView subclass called MKPinAnnotationView. 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 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!
(Hint: in the land of the Wire, you won’t have to search very far! :])

Opening the Maps app

Wanna try out a cool new feature in iOS 6? You can now easily launch the Maps app right from within your app, with parameters to configure exactly what to show!
Remember that mapItem method you added to MyLocation? Well now you’re about to use it! OpenViewController.m and add the following code:
// Add to mapView:viewForAnnotation: after setting the image on the annotation view
annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
 
// Add the following method
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control {
    MyLocation *location = (MyLocation*)view.annotation;
 
    NSDictionary *launchOptions = @{MKLaunchOptionsDirectionsModeKey : MKLaunchOptionsDirectionsModeDriving};
    [location.mapItem openInMapsWithLaunchOptions:launchOptions];
}
You’ve now made it so that in the callout when a pin is tapped on, there will be a button on the right hand side. When this is tapped, the mapView:annotationView:calloutAccessoryControlTapped: method is called. In this method, you grab the MyLocation object that this tap refers to and then launch the Maps app by calling the openInMapsWithLaunchOptions: method.
It’s as simple as that! You’ll notice that a dictionary is passed in to this method. This allows you to specify a few different options; here the directions key is set to driving. This will make the Maps app try to give driving directions from the current location to this pin. Neat!
I suggest you take a look at the various different options you can pass in the launch options dictionary. Also take a look at the class method on MKMapItem to allow you to pass multiple MKMapItem objects at the same time.
Now build & run the app, perform a refresh and tap on an arrest item. Then tap the blue button on the right and watch it launch the Maps app to show that arrest, with driving directions to it. Very cool :]!
Note: You may have gotten an error when opening the Maps app. If so, it’s likely because it couldn’t get driving directions for some reason. To fix that, simulate your location as being somewhere in the US (e.g. San Francisco) and try again. This time it’ll work and show you directions from downtown San Francisco right to the arrest!

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:
// 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! :]

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

No comments:

Post a Comment