Geolocation and Mapping - Learning iPhone Programming (2010)

Learning iPhone Programming (2010)

Chapter 11. Geolocation and Mapping

The Core Location API is one of the great things about the iPhone and iPod touch platforms, but until the arrival of the MapKit Framework in the 3.0 SDK, it was actually quite hard to take that location-aware goodness and display it on a map. The arrival of the MapKit framework has simplified this enormously.

Let’s work through a few example applications to get you familiar with the framework.

User Location

Note

You can follow along while I build this application in a screencast available on the book’s website.

The first thing we’re going to do is build a simple application to answer the question “Where am I?”. Start a new iPhone project in Xcode, select a view-based template, and name the project “WhereAmI” when prompted.

Next, you need to add the MapKit and Core Location frameworks to your new project. You do not need the Core Location framework to work with MapKit, but we’re going to use it later in the chapter, so we may as well add it now:

1. Right-click on the Frameworks group in the Groups & Files pane in Xcode and select Add→Existing Frameworks. In the pop-up window that appears, select the MapKit framework and click Add.

2. Do this a second time, but for the Core Location framework.

Warning

If you have upgraded your Xcode (and iPhone SDK) distribution in the middle of developing a project, MapKit.framework may not show up in the list of frameworks Xcode presents in the framework selection pop up. In this case, you may be able to resolve the problem by opening the Targets group in the Groups & Files pane in Xcode, right-clicking on the application’s target, and selecting Get Info. Navigate to the Build pane of the Target Info window and set the Base SDK of your project to the SDK you currently have installed (rather than the SDK with which you initially developed the project).

If this doesn’t resolve the problem, you may have to add the framework manually. Click on the Add Other button in the bottom left of the window. The MapKit.framework framework is located in the/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/<iPhoneOSX.X.sdk>/System/Library/Frameworks/ directory and you should be able to add it manually. Replace <iPhoneOSX.X.sdk> with your current SDK version.

Once that’s done, click on the WhereAmIViewController.h interface file to open it in the Xcode editor and add a map view instance to the class, along with the imports needed for Core Location and MapKit:

#import <UIKit/UIKit.h>

#import <MapKit/MapKit.h>

#import <CoreLocation/CoreLocation.h>

@interface WhereAmIViewController : UIViewController {

MKMapView *mapView;

}

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

@end

Then click on the corresponding implementation file (WhereAmIViewController.m) to open it in the Xcode editor. Make sure you synthesize the mapView property, remove the /* and */ comment delimiters from viewDidLoad:, and release the mapView property in the dealloc:method:

#import "WhereAmIViewController.h"

@implementation WhereAmIViewController

@synthesize mapView;

- (void)viewDidLoad {

[super viewDidLoad];

}

- (void)didReceiveMemoryWarning {

[super didReceiveMemoryWarning];

}

- (void)viewDidUnload {

}

- (void)dealloc {

[mapView release];

[super dealloc];

}

@end

Save your changes to the WhereAmIViewController class and double-click on the WhereAmIViewController.xib file to open it in Interface Builder. Drag and drop an MKMapView from the Library window into the View window. Now click on File’s Owner, select the Attributes Inspector (⌘-2), and connect the mapView outlet to the MKMapView, as shown in Figure 11-1.

Connecting the mapView outlet to the MKMapView

Figure 11-1. Connecting the mapView outlet to the MKMapView

We’re done for now in Interface Builder. Save your changes to the NIB file and go back into Xcode and click the Build and Run button on the Xcode toolbar to build and deploy your application in iPhone Simulator. You should see something similar to Figure 11-2.

The default map view in iPhone Simulator

Figure 11-2. The default map view in iPhone Simulator

It’s not amazingly interesting so far, so let’s use Core Location to change that.

While MapKit knows the current user location and can mark it on the map (you’ll see the property that enables this, showsUserLocation, in the didUpdateToLocation:fromLo⁠cation: method shortly), there is no way to monitor it or update the current map view when the location changes. So, we’re going to implement an application that uses Core Location to determine and zoom to the current location and then display the standard user location marker using MapKit.

Click on the WhereAmIAppDelegate.h interface file to open it in the Xcode editor. We’re going to declare that the application delegate also implements the CLLocationManagerDelegate protocol, and add a locationManager property to the class declaration. Make the changes shown in bold to this interface file:

#import <UIKit/UIKit.h>

#import <CoreLocation/CoreLocation.h>

@class WhereAmIViewController;

@interface WhereAmIAppDelegate : NSObject

<UIApplicationDelegate, CLLocationManagerDelegate>

{

UIWindow *window;

CLLocationManager *locationManager;

WhereAmIViewController *viewController;

}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@property (nonatomic, retain) IBOutlet CLLocationManager *locationManager;

@property (nonatomic, retain) IBOutlet WhereAmIViewController *viewController;

@end

In the implementation file (WhereAmIAppDelegate.m), we need to create an instance of the location manager and start updating our location (see The Core Location Framework in Chapter 10 for an overview of the location manager):

#import "WhereAmIAppDelegate.h"

#import "WhereAmIViewController.h"

@implementation WhereAmIAppDelegate

@synthesize window;

@synthesize locationManager;

@synthesize viewController;

- (void)applicationDidFinishLaunching:(UIApplication *)application {

self.locationManager = [[[CLLocationManager alloc] init] autorelease];

if ( self.locationManager.locationServicesEnabled ) {

self.locationManager.delegate = self;

self.locationManager.distanceFilter = 1000;

[self.locationManager startUpdatingLocation];

}

[window addSubview:viewController.view];

[window makeKeyAndVisible];

}

- (void)dealloc {

[viewController release];

[window release];

[super dealloc];

}

@end

Now we must implement the locationManager:didUpdateToLocation:fromLocation: delegate method. Add the following to WhereAmIAppDelegate.m:

- (void)locationManager:(CLLocationManager *)manager

didUpdateToLocation:(CLLocation *)newLocation

fromLocation:(CLLocation *)oldLocation {

double miles = 12.0;

double scalingFactor =

ABS( cos(2 * M_PI * newLocation.coordinate.latitude /360.0) );

MKCoordinateSpan span;

span.latitudeDelta = miles/69.0;

span.longitudeDelta = miles/( scalingFactor*69.0 );

MKCoordinateRegion region;

region.span = span;

region.center = newLocation.coordinate;

[viewController.mapView setRegion:region animated:YES];

viewController.mapView.showsUserLocation = YES;

}

Here we set the map region to be 12 miles square, centered on the current location. Then we zoom in and display the current user location.

Note

The number of miles spanned by a degree of longitude range varies based on the current latitude. For example, one degree of longitude spans a distance of ~69 miles at the equator but shrinks to 0 at the poles. However, unlike longitudinal distances, which vary based on the latitude, one degree of latitude is always ~69 miles (ignoring variations due to the slightly ellipsoidal shape of Earth).

Length of 1 degree of Longitude (miles) = cosine (latitude) × 69 (miles)

Click the Build and Run button on the Xcode toolbar to build and deploy your application in iPhone Simulator. You should see something like Figure 11-3.

The map view showing the current user location

Figure 11-3. The map view showing the current user location

Before leaving this example, let’s add one more feature to display the current latitude and longitude on top of the map. Open the WhereAmIViewController.h interface file and add two outlets to UILabel for the latitude and longitude values:

@interface WhereAmIViewController : UIViewController {

MKMapView *mapView;

UILabel *latitude;

UILabel *longitude;

}

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

@property (nonatomic, retain) IBOutlet UILabel *latitude;

@property (nonatomic, retain) IBOutlet UILabel *longitude;

@end

Since we’ve added these two properties, we need to synthesize them in the corresponding implementation file, and additionally remember to release them in the dealloc: method. Make the changes shown in bold to WhereAmIViewController.m:

@implementation WhereAmIViewController

@synthesize mapView;

@synthesize latitude;

@synthesize longitude;

... some code not shown ...

- (void)dealloc {

[mapView release];

[latitude release];

[longitude release];

[super dealloc];

}

@end

Make sure you’ve saved those changes and double-click on the WhereAmIViewController.xib file to open it in Interface Builder. Drag and drop a round rect button (UIButton) onto the view, resizing it roughly to the size shown in Figure 11-4.

We’re going to use the button as a backdrop for latitude and longitude labels. It’s actually a fairly common trick to do this as it gives a nice box with rounded corners, but you must uncheck the User Interaction Enabled box in the View section of the Attributes Inspector (⌘-1). This will disable the user’s ability to select the button. If you’re uncomfortable doing this, you could equally well use a UIImage as a backdrop, or simply set the UILabel backgrounds to white or another appropriate color.

Next, drag and drop two labels from the Library onto the button in the View window and change the label contents to be “Latitude” and “Longitude”. Finally, drag and drop two more labels onto the button and position them next to the previous two and set the contents to be blank. Now click on File’s Owner, go to the Attributes tab of the Inspector window, and connect the longitude and latitude outlets to your two blank labels, as shown in Figure 11-4.

Connecting the label outlets in Interface Builder

Figure 11-4. Connecting the label outlets in Interface Builder

Save your changes to the NIB file. Back in Xcode, click on the WhereAmIAppDelegate.m file to open it in the Xcode editor. Now all you have to do is populate the two labels you added. In the locationManager:didUpdateToLocation:fromLocation: method, add the lines shown in bold:

- (void)locationManager:(CLLocationManager *)manager

didUpdateToLocation:(CLLocation *)newLocation

fromLocation:(CLLocation *)oldLocation

{

MKCoordinateSpan span;

span.latitudeDelta = 0.2;

span.longitudeDelta = 0.2;

MKCoordinateRegion region;

region.span = span;

region.center = newLocation.coordinate;

[viewController.mapView setRegion:region animated:YES];

viewController.mapView.showsUserLocation = YES;

viewController.latitude.text =

[NSString stringWithFormat:@"%f", newLocation.coordinate.latitude];

viewController.longitude.text =

[NSString stringWithFormat:@"%f", newLocation.coordinate.longitude];

}

Make sure you’ve saved your changes and click the Build and Run button in the Xcode toolbar. If all goes well, you should be presented with a view that looks similar to Figure 11-5.

The current user location

Figure 11-5. The current user location

Annotating Maps

Like we did for the UIWebView in Chapter 7, here we’re going to build some code that you’ll be able to reuse in your own applications later. We’re going to build a view controller that we can display modally, and which will display an MKMapView annotated with a marker pin and can then be dismissed, returning us to our application.

We can reuse the Prototype application code we built in Chapter 7, which I used to demonstrate how to use the web and mail composer views. Open the Finder and navigate to the location where you saved the Prototype project. Right-click on the folder containing the project files and select Duplicate; a folder called Prototype copy will be created containing a duplicate of our project. Rename the folder Prototype3, and just as we did when we rebuilt the Prototype application to demonstrate the mail composer, prune the application down to the stub with the Go! button and associated pushedGo: method we can use to trigger the display of our map view (see Sending Email in Chapter 7 for details).

Now, right-click on the Classes group in the Groups & Files pane, select Add→New File, and select Cocoa Touch Class from the iPhone section. Create a UIViewController subclass, leaving the “With XIB for user interface” checkbox ticked. Name the new class “MapViewController” when prompted.

Note

At this point, I normally rename the NIB file that Xcode automatically created, removing the “Controller” part of the filename and leaving it as MapView.xib, as I feel this is a neater naming scheme.

You’ll need to add both the MapKit and the Core Location frameworks to your project, as you did in the preceding section, so that you can use the classes these frameworks offer.

Note

We’re going to be using the Core Location and MapKit frameworks throughout this project; instead of having to include them every time we need them we can use the Prototype_prefix.pch header file to import them into all the source files in the project. Open this file (it’s in the Other Sources group) and change it to read as follows:

#ifdef OBJC__

#import <Foundation/Foundation.h>

#import <UIKit/UIKit.h>

#import <CoreLocation/CoreLocation.h>

#import <MapKit/MapKit.h>

#endif

This file is called a prefix file because it is prefixed to all of your source files. However, the compiler precompiles it separately; this means it does not have to reparse the file on each compile run, which can dramatically speed up your compile times on larger projects.

Let’s start by creating the UI for the new map view. Double-click on the MapView.xib file to open the NIB file in Interface Builder. Drag and drop a navigation bar (UINavigationBar) from the Library window, positioning it at the top of the view. Then drag a map view (MKMapView) into the view and resize it to fill the remaining portion of the View window. Finally, drag a bar button item (UIBarButtonItem) onto the navigation bar, and in the Attributes Inspector (⌘-1) change its Style and Identifier to Done in the Bar Button Item section of the tab. At this point, your view should look similar to Figure 11-6.

Creating our map view in Interface Builder

Figure 11-6. Creating our map view in Interface Builder

After saving the changes to the MapView.xib file, close it and return to Xcode. Open the MapViewController.h interface file. Just as we did for the web view, we want to make this class self-contained so that we can reuse it without any modifications. Therefore, override the init:function again to pass the information you need when instantiating the object:

#import <UIKit/UIKit.h>

@interface MapViewController : UIViewController <MKMapViewDelegate> {

CLLocationCoordinate2D theCoords;

NSString *theTitle;

NSString *theSubTitle;

IBOutlet MKMapView *mapView;

IBOutlet UINavigationItem *mapTitle;

}

- (id) initWithCoordinates:(CLLocationCoordinate2D)coordinates;

- (id) initWithCoordinates:(CLLocationCoordinate2D)coordinates

andTitle:(NSString *)title;

- (id) initWithCoordinates:(CLLocationCoordinate2D)coordinates

andTitle:(NSString *)title andSubTitle:(NSString *)subtitle;

- (IBAction) done:(id)sender;

@end

I’ve actually provided three independent init methods; which one you use depends on how much metadata you want to pass to the MapViewController class. If you look at the corresponding implementation in the MapViewController.m file, you’ll notice that I’ve really only coded one of them. The other two are simply convenience methods that are chained to the first:

#import "MapViewController.h"

@implementation MapViewController

- (id) initWithCoordinates:(CLLocationCoordinate2D)coordinates

andTitle:(NSString *)title andSubTitle:(NSString *)subtitle

{

if ( self = [super init] ) {

theTitle = title;

theSubTitle = subtitle;

theCoords = coordinates;

}

return self;

}

- (id) initWithCoordinates:(CLLocationCoordinate2D)coordinates

andTitle:(NSString *)title

{

return [self initWithCoordinates:coordinates

andTitle:title andSubTitle:nil];

}

- (id) initWithCoordinates:(CLLocationCoordinate2D)coordinates

{

return [self initWithCoordinates:coordinates

andTitle:nil andSubTitle:nil];

}

- (IBAction) done:(id)sender {

[self dismissModalViewControllerAnimated:YES];

}

- (void)viewDidLoad {

[super viewDidLoad];

mapTitle.title = theTitle;

// code to add annotations goes here later

}

- (void)didReceiveMemoryWarning {

[super didReceiveMemoryWarning];

}

- (void)dealloc {

[theTitle release];

[theSubTitle release];

[mapView release];

[mapTitle release];

[super dealloc];

}

@end

Save your changes and click on the PrototypeViewController.m implementation file to open it in the Xcode editor. Import the MapViewController class:

#import "MapViewController.h"

Then replace the pushedGo: method with the following:

-(IBAction) pushedGo:(id)sender {

CLLocationCoordinate2D coord = {37.331689, -122.030731};

MapViewController *mapView =

[[MapViewController alloc] initWithCoordinates:coord

andTitle:@"Apple"

andSubTitle:@"1 Infinite Loop"];

[self presentModalViewController:mapView animated:YES];

[mapView release];

}

Now we have to go back into Interface Builder and connect the web view up to our controller code. Open the MapView.xib file in Interface Builder and make sure the view mode is in list mode (⌘-Option-2). Expand all the nodes by Option-clicking on the disclosure triangle to the left of the view. Next, click on File’s Owner and follow these steps:

1. In the Connections Inspector (⌘-2), connect the mapTitle outlet to the UINavigationItem “Navigation Item (Title)”.

2. Connect the mapView outlet to the MKMapView.

3. Connect the done: received action to the UIBarButtonItem “Bar Button Item (Done)”.

4. Click on the map view and connect the delegate outlet back to File’s Owner.

At this point, if you click on File’s Owner in the main NIB window and check the Connections tab, you should see something very much like Figure 11-7.

The map view NIB file connected to the MapViewController

Figure 11-7. The map view NIB file connected to the MapViewController

It’s time to stop and test our application. Save the NIB file, return to Xcode, and click on the Build and Run button to compile and start the application in iPhone Simulator. Tap the Go! button and the map view should load. Right now we haven’t specified any annotations, or a region, so you should just see a default world map (see Figure 11-8).

Let’s change that. The first thing we need to do is create a class that implements the MKAnnotation protocol. Right-click on the Classes group in the Groups & Files pane, select Add→New File, and create a new Objective-C class (an NSObject subclass). Name the new class “SimpleAnnotation” when prompted.

Open the SimpleAnnotation.h interface file Xcode has just created in the editor and modify it as follows:

#import <Foundation/Foundation.h>

@interface SimpleAnnotation : NSObject

<MKAnnotation>

{

CLLocationCoordinate2D coordinate;

NSString *title;

NSString *subtitle;

}

@property (nonatomic, assign) CLLocationCoordinate2D coordinate;

@property (nonatomic, retain) NSString *title;

@property (nonatomic, retain) NSString *subtitle;

+ (id)annotationWithCoordinate:(CLLocationCoordinate2D)coord;

- (id)initWithCoordinate:(CLLocationCoordinate2D)coord;

@end

The initial main view (left) and the web view (right)

Figure 11-8. The initial main view (left) and the web view (right)

Then open the corresponding SimpleAnnotation.m implementation file, and make the changes shown here:

#import "SimpleAnnotation.h"

@implementation SimpleAnnotation

@synthesize coordinate;

@synthesize title;

@synthesize subtitle;

+ (id)annotationWithCoordinate:(CLLocationCoordinate2D)coord {

return [[[[self class] alloc] initWithCoordinate:coord] autorelease];

}

- (id)initWithCoordinate:(CLLocationCoordinate2D)coord {

if ( self = [super init] ) {

self.coordinate = coord;

}

return self;

}

- (void)dealloc {

[title release];

[subtitle release];

[super dealloc];

}

@end

The SimpleAnnotation class is just a container; it implements the MKAnnotation protocol to allow it to hold the coordinates and title (with subtitle) of our annotation.

Save your changes and click on the MapViewController.m implementation file to open it in the Xcode editor. Import the SimpleAnnotation class:

#import "SimpleAnnotation.h"

Edit the viewDidLoad: method to add the annotation using theCoords, theTitle, and theSubTitle passed to the MapViewController when it was initialized:

- (void)viewDidLoad {

[super viewDidLoad];

mapTitle.title = theTitle;

SimpleAnnotation *annotation =

[[SimpleAnnotation alloc] initWithCoordinate:theCoords];

annotation.title = theTitle;

annotation.subtitle = theSubTitle;

MKCoordinateRegion region = { theCoords, {0.2, 0.2} };

[mapView setRegion:region animated:NO];

[mapView addAnnotation: annotation];

[annotation release];

}

We’re done. Make sure all your changes are saved, and click the Build and Run button in the Xcode toolbar to build and deploy your application in iPhone Simulator. If all goes well, clicking on the Go! button should give you a view that looks like Figure 11-9.

The finished MapViewController and its view

Figure 11-9. The finished MapViewController and its view

At this point, you have reusable MapViewController and SimpleAnnotation classes, along with an associated NIB file that you can drag and drop directly into your own projects.

You might want to think about some improvements if you do that, of course. For instance, you could easily expand the class to handle multiple annotations. While the annotations themselves can provide a much richer interface than a simple pushpin, look at the documentation for theMKAnnotationView class for some inspiration.