Using the Map framework
Specifying the location and zoom level of a map
Annotating significant locations on the map
Identifying the iPhone's current location
One of the things that makes iPhone applications compelling is the ability you have as a developer to incorporate the user's location into the application functionality. And one of the more compelling ways to do that is through the use of maps.
Including the ability to display a map in RoadTrip became important as people began to realize the kinds of solutions that can be delivered on the iPhone. To many travelers, nothing brands you more as a tourist than unfolding a large paper map (except of course looking through a thick guidebook). In this chapter, I show you how to take advantage of the iPhone's built-in capability to display a map of virtually anywhere in the world, as well as determine the iPhone's location and then indicate it in the map. As I mention way back in Book I, the iPhone's awareness of your location is one of the things that enables you to develop a totally new kind of application and really differentiate an iPhone application from a desktop one.
Being able to build maps into your application is an important new feature in the iPhone 3.0 SDK and beyond, and it doesn't hurt that working with maps is one of the funnest things you can do on the iPhone because Apple makes it so easy.
In this chapter, I show you how to center your map on an area you want to display (San Francisco for example), add annotations (those cute pins in the map that display a callout to describe that location when you touch them), and even show the user's current location. (Although I don't cover it until Book VII, you can also turn the iPhone's current address into an Address Book contact.)
To use maps, you have to add a few more files to your project — a MapController.h
and .m
, a MapController
nib file, and model files Map.h
and .m
, to be precise. (You'd already done something similar in Chapter 4 of this minibook, so if this talk of nib files and model files doesn't ring a bell, you might want to read this minibook's Chapter 4 — the figures there may prove especially helpful in jogging your memory.) You won't be using the Map
files in this Chapter, but you will in Book VI, so you might as well create them now.
In the RoadTrip Project window, select the Classes folder and then choose File
In the left column of the dialog, select Cocoa Touch Classes under the iPhone OS heading, select the UIViewController subclass template in the top-right pane, and be sure the With XIP for User Interface check box is also selected. Then click Next.
You see a new dialog asking for some more information.
Enter MapController.m in the File Name field and then click Finish.
Choose File
In the leftmost column of the dialog, select Cocoa Touch Classes under the iPhone OS heading just like you did before, but this time select the Objective-C class template in the topmost pane, making sure that the Subclass drop-down menu has NSObject selected. Then click Next.
You see a new dialog asking for some more information.
Enter Map in the File Name field and then click Finish.
Okay, that takes care of your MapController.m
and .h files and your Map.m
and .h
files. One thing left to do: You have to add a new framework.
Up until now, all you've needed is the framework that more or less came supplied when you created a project. But now, you need to add a new framework to enable the Map view. (Officially it is a MKMapView
but I'll refer to it as simply a Map view.)
Click the disclosure triangle next to Targets in the Groups & Files list and then right-click on RoadTrip.
Be sure to do this using the Targets folder, or Step 3 won't work!
From the submenu that appears, select Add and then select Existing Frameworks, as I've done in Figure 5-1.
In the new window that appears (see Figure 5-2), select MapKit.framework
and then click Add. It will go to the very bottom of your RoadTrip project files. From there, you can drag it into the Frameworks folder.
Just as in Chapter 4 of this minibook, you need to add some initialization code to get things rolling
It all starts in RootViewController.m
.
In the RootViewController.m
file's didSelectRowAtIndexPath:
method, create and initialize the view controller that implements the row selected by the user.
The code is already there, so all you need to do is uncomment it out, but it doesn't hurt to review it here.
The following code allocates a MapController
(a view controller) and then sends it the initWithTrip:trip:
message. (I explain this process at the very end of Chapter 2 of this minibook; you might want to review that if it's been a while since you looked at it.)
if (realtime) targetController = [[MapController alloc] initWithTrip:trip]; else [self displayOfflineAlert: [[menuList objectAtIndex:menuOffset] objectForKey:kSelectKey]];
Notice the alert in the third line; I address this use of alerts when I talk about the Weather alert in Chapter 4 of this minibook. To use a map, your user needs to be online; that means you need to build in an alert that lets the user know whether he or she is in fact not online.
You also need to import the MapController.h
and add it to the RootViewController.m
file. Lucky for you, it's real easy. Just add the following:
#import "MapController.h"
Make the MapController
an MKMapViewDelegate
by adding the code in bold shown in this step to the MapView.h
file.
Later when you do reverse geocoding (getting a street address for a coordinate) you also have to become an MKReverseGeocoderDelegate
, so you might as well do that now.
@interface MapController : UIViewController <
MKMapViewDelegate, MKReverseGeocoderDelegate
> {
Declare a new instance variable in MapController.h
.
By now you should know you can put it anywhere between the braces. This particular instance variable is there to connect the MapController
to the Trip
model.
Trip *trip;
Add the method declaration for the initialization method to MapController.h
, being sure to add it after the }
but before the @end
statement.
- (id) initWithTrip: (Trip*) theTrip;
Add the necessary @class
and #import
statements.
#import <MapKit/MapKit.h>
gives you access to all the lovely functionality that came your way when you added the MapKit.framework
in the last section
#import <MapKit/MapKit.h> @class Trip;
Add a mapView (UIWebView)
outlet to the MapController.h
interface file.
IBOutlet MKMapView *mapView;
You get a default map for free (see Figure 5-3), which is all well and good, but there's a lot more than you can do with it. For that to happen, though, you're going to need to be able to access the Map view. To do that, do the right thing and follow this step: Declare an outlet by using the keyword IBOutlet
in the MapController.h
interface file.
Initialize the MapController
.
Add the following initWithTrip:
method to MapController.m
- (id) initWithTrip: (Trip*) theTrip { if (self = [super initWithNibName:@"MapController" bundle:nil]) { trip = theTrip; } return self; }
then add the #import
statement to MapController.m
.
#import "Trip.h"
Do the File
After it's saved — and only then — Interface Builder can find the new outlet.
If you were to compile and run RoadTrip now, you would be able to select Map in the Main view . . . but all you would see is a blank page. To see a map, you need to do some work in the nib file.
For the RoadTrip application, you want to use a MKMapView
to display the map information. To set up the MKMapView
, you use Interface Builder. In fact, if all you did in Interface Builder was change the UIView
to an MKMapView
, you'd get the map you see in Figure 5-3. It doesn't get much easier than that.
This is the general approach you'll follow when you add more functionality to your application — add the new controller classes and their nib files, and new model classes to the model.
Use the Groups & Files list on the left in the Project window to drill down to the MapController.xib
file; then double-click the file to launch it in Interface Builder.
If the Attributes Inspector window is not open, choose Tools
If for some reason you can't find the MapController.xib
window (you may have minimized it whether by accident, on purpose, or whatever), you can get it back by choosing Window
Select File's Owner in the MapController.xib
window.
It should already be set to MapController
. If not, retrace your steps to see where you may have made a mistake.
You need to be sure that the File's Owner is MapController
. You can set File's Owner from the Class drop-down menu in the Identity Inspector.
Click in the View window and then choose MKMapView from the Class drop-down menu in the Identity Inspector.
The name in the MapController.xib
window will change to Map View, and the title of the View window will change to Map View the next time you reopen the window after it has been closed.
Back in the MapController.xib
window, right-click File's Owner to call up a contextual menu with a list of connections.
You can get the same list using the Connections tab in the Attributes Inspector.
Drag from the little circle next to the mapView
outlet in the list onto the Map View window.
Doing so connects the MapController
's mapView
outlet to the Map view.
Go back to that list of connections in the File's Owner contextual menu and click the triangle next to Referencing Outlets. This time drag from the little circle next to the New Referencing Outlet list onto the Map View window.
You may recall that you did the exact same thing with the Web view in Chapter 4 of this minibook.
With the cursor still in the Map View window, let go of the mouse button.
A pop-up menu appears.
Choose Delegate from the pop-up menu.
Do the File
The MapController
now has its outlet to the Map view connected, and it also will receive the delegate messages as a MKMapViewDelegate
. I'll show you what methods you'll need to implement next.
Only after the file's saved will the changes you made be reflected in your application.
If you were to build and run your program at this point, you'd still get the default Map view you see in Figure 5-3. But you — and your users — want and deserve more than that. Figure 5-4 shows what you'd like to see on your road trip, rather than the standard Map view you get right out of the box.
You've prepared the ground for some great map functionality, but now it's time to put the code in place so that you can get some real work done. Undergirding all this effort is the MapKit.framework
— surely one of the great features of iPhone 3.0 SDK and beyond is a new framework. MapKit enables you to bring up a simple map and also do things with your map without having to do much work at all.
The map looks like the maps in the built-in applications and creates a seamless mapping experience across multiple applications.
The essence of mapping on the iPhone is the MKMapView
. It's a UIView
subclass, and as you saw in the previous section, you can use it out of the box to create a world map. You use this class as-is to display map information and to manipulate the map contents from your application. It enables you to center the map on a given coordinate, specify the size of the area you want to display, and annotate the map with custom information.
You added the MapKit.framework
earlier in this chapter.
When you initialize a Map view, you can specify the initial region for that map to display. You do this by setting the region property of the map. A region is defined by a center point and a horizontal and vertical distance, referred to as the span. The span defines how much of the map will be visible and also determines the zoom level. The smaller the span, the greater the zoom.
The Map view supports the standard map gestures.
Scroll
Pinch zoom
Double-tap zoom in
Two-finger-tap zoom out (You may not even have known about that one.)
You can also specify the map type — regular, satellite, or hybrid — by changing a single property.
Because MapKit.framework
was written from scratch, it was developed with the limitations of the iPhone in mind. As a result, it optimizes performance on the iPhone by caching data as well as managing memory and seamlessly handling connectivity changes (like moving from 3g to Wi-Fi, for example).
The map data itself is Google-hosted map data, and network connectivity is required. And because MapKit.framework
uses Google services to provide map data, using it binds you to the Google Maps/Google Earth API terms of service.
Although you shouldn't subclass the MKMapView
class itself, you can tailor a Map view's behavior by providing a delegate object. The delegate object can be any object in your application, as long as it conforms to the MKMapViewDelegate
protocol. (You may find out how to make the MapController
the MKMapView
delegate in the preceding section.)
Having this nice global map centered on the United States is kind of interesting but not very useful if you're planning to go to San Francisco. The following sections show you what you would have to do to make the map more useful.
To start with, it would be very useful to be able to see any map in landscape mode.
Go back to your Project window in Xcode and add the following method to MapController.m
:
- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientation { return YES; }
That's all you have to do to view the map in landscape mode. You can move back and forth between landscape and portrait mode and MapKit.framework
takes care of it for you! (This is starting to be real fun.)
What about showing your location on the map? That's just as easy!
In the MapController.m
file, uncomment out viewDidLoad
and add the code in bold.
- (void)viewDidLoad {
[super viewDidLoad];
mapView.showsUserLocation = YES;
}
showsUserLocation
is a MKMapView
property that tells the Map view whether to show the user location. If YES
, you get that same blue pulsing dot displayed in the built-in Map application.
If you were to compile and run the application as it stands, you'd get what you see in Figure 5-5 — a map of the USA in landscape mode with a blue dot that represents the phone's current location. (There may be a lag until the iPhone is able to determine that location, but you should see it eventually.) Of course, to see it in landscape mode, you have to turn the iPhone, or choose Hardware
If you don't see the current location, you might want to check and make sure you've connected the mapView
outlet to the Map view in the nib file — see the "Setting up the nib file" section, earlier in the chapter.
You get your current location if you are running your app on the iPhone. If you're running it on the Simulator, that location is Apple — beautiful, Cupertino, California, to be precise. Touching on the blue dot also displays what's called an annotation, and I tell you how to customize the text to display whatever you cleverly come up with — including, as you discover in the upcoming "Annotations" section, the address of the current location.
Okay, now you've got a blue dot on a map of the good ol' US of A. Cute, but still not that useful for the purposes of the app.
As I mention at the beginning of this chapter, ideally, when you get to San Francisco (or wherever), you should see a map that centers on San Francisco as opposed to the United States. To get there from here, however, is also pretty easy.
First you need to look at how you center the map.
Back in your Project window, add the following code to MapController.m
:
- (void)updateRegionLatitude:(float) latitude longitude:(float) longitude latitudeDelta:(float) latitudeDelta longitudeDelta:(float) longitudeDelta { MKCoordinateRegion region; region.center.latitude = latitude; region.center.longitude = longitude; region.span.latitudeDelta = latitudeDelta; region.span.longitudeDelta = longitudeDelta; [mapView setRegion:region animated:NO]; }
Also add the declaration to the MapController.h
file.
Setting the region is how you center the map and set the zoom level. You accomplish all this with the following statement:
[mapView setRegion:region animated:NO];
A region is a Map View property that specifies four things (as illustrated in Figure 5-6).
region.center.latitude
specifies the latitude of the center of the map.
region.center.longitude
specifies the longitude of the center of the map.
For example, if I were to set those values as
region.center.latitude = 37.774929; region.center.longitude = −122.419415;
the center of the map would be San Francisco.
region.span.latitudeDelta
specifies the north-to-south distance (in latitudinal degrees) to display on the map. One degree of latitude is approximately 111 kilometers (69 miles). A region.span.latitudeDelta
of 0.0036 would specify a north-to-south distance on the map of about a quarter of a mile. Latitudes north of the equator have positive values, whereas latitudes south of the equator have negative values.
region.span.longitudeDelta
specifies the east-to-west distance (in longitudinal degrees) to display on the map. Unfortunately, the number of miles in one degree of longitude varies based on the latitude. For example, one degree of longitude is approximately 69 miles at the equator but shrinks to 0 miles at the poles. Longitudes east of the zero meridian (by international convention, the zero or Prime Meridian passes through the Royal Observatory, Greenwich, in east London) have positive values, and longitudes west of the zero meridian have negative values.
Although the span values provide an implicit zoom value for the map, the actual region you see displayed may not equal the span you specify because the map will go to the zoom level that best fits the region that is set. This also means that even if you just change the center coordinate in the map, the zoom level may change because distances represented by a particular span may change at different latitudes and longitudes. To account for that, those smart developers at Apple included a property you can set that will change the center coordinate without changing the zoom level.
@property (nonatomic) CLLocationCoordinate2D centerCoordinate
When you change the value of this property with a new CLLocationCoordinate2D
, the map is centered on the new coordinate, and updates span values to maintain the current zoom level.
That CLLocationCoordinate2D
type is something you'll be using a lot, so I'd like to explain that before I take you any further.
CLLocationCoordinate2D
type is a structure that contains a geographical coordinate using the WGS 84 reference frame (the reference coordinate system used by the Global Positioning System).
typedef struct { CLLocationDegrees latitude; CLLocationDegrees longitude; } CLLocationCoordinate2D;
Here's a little explanation:
latitude
is the latitude in degrees. This is the value you set in the code you just entered (region.center.latitude = latitude;
).
longitude
is the longitude in degrees. This is the value you set in the code you just entered (region.center.longitude = longitude;
).
To center the map display on San Francisco, you send the updateRegionLatitude:longitude: latitudeDelta:longitudeDelta
message (the code you just entered) when the view is loaded in the viewDidLoad:
method. You already added some code there to display the current location, so add the code in bold to MapController.m
.
- (void)viewDidLoad { [super viewDidLoad]; mapView.showsUserLocation = YES;CLLocationCoordinate2D initialCoordinate =
[trip initialCoordinate];
[self updateRegionLatitude:
initialCoordinate.latitude
longitude:initialCoordinate.longitude
latitudeDelta:.06 longitudeDelta:.06];
self.title = [trip mapTitle];
}
Take a look at what adding the bold stuff does:
The initialCoordinate
message is sent to the Trip
object (remember your model from Chapter 4 in this minibook ) to get the initial coordinates you want displayed. You're adding some additional functionality to the model, whose responsibility now includes specifying that location. The user may have requested that location when he or she set up the trip (I don't cover that topic in this book, leaving it as an exercise for the reader), or it may have been a default location that you decided on when you wrote the code (an airport specified in the destination, for example).
Your code now sets the map title by sending the mapTitle
message to the Trip
object — adding another model responsibility. (This gives you a chance to title the map based on whatever criteria you would like, such as the current location.)
- (NSString*) mapTitle{ return @"Sites"; }
For all of this to work, of course, you have to add the following code to Trip.m
. This code returns the latitude and longitude for San Francisco.
- (CLLocationCoordinate2D)initialCoordinate { CLLocationCoordinate2D startCoordinate; startCoordinate.latitude = 37.774929; startCoordinate.longitude = −122.419415; return startCoordinate; } - (NSString*) mapTitle{ return @" map"; }
You of course have to include the MapKit
in Trip
, so add the following to Trip.h
:
#import <MapKit/MapKit.h>
You also have to add the following to Trip.h
(just stick it in after the braces):
- (CLLocationCoordinate2D)initialCoordinate; - (NSString*)mapTitle;
If you compile and build your project, you should see what's shown in Figure 5-7.
But what if the Internet isn't available? The Apple Human Interface Guidelines (and common sense) say that you should keep the user informed of what's going on. By virtue of the fact that you've made the MapController
a MKMapView
delegate, your app is in the position to send a message in the event of a load failure. Adding the following code to the MapController.m
file makes it final:
- (void)mapViewDidFailLoadingMap:(MKMapView *)mapView withError:(NSError *)error { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Unable to load the map" message:@"Check to see if you have internet access" delegate:self cancelButtonTitle: @"Thanks" otherButtonTitles:nil]; [alert show]; [alert release]; }
Testing this alert business on the Simulator doesn't always work because it does some caching. You're better off testing it on the device itself by turning on Airplane Mode.
At this point, when the user touches Map in the Main view, RoadTrip displays a map centered on San Francisco, and if you pan over to Cupertino (or wherever you are), you can see the blue dot.
The MKMapView
class supports the ability to annotate the map with custom information. There are two parts to the annotation — the annotation itself, which contains the data for the annotation, and the Annotation view that displays the data.
An annotation plays a similar role to the dictionary you created in Chapter 2 of this minibook, where the dictionary was meant to hold the text to be displayed in the cell of a table view. Both dictionaries and annotations act as models for their corresponding view, with a view controller connecting the two.
Annotation objects are any object that conforms to the MKAnnotation
protocol and are typically existing classes in your application's model. The job of an Annotation object is to know its location (coordinate) on the map along with the text to be displayed in the callout. The MKAnnotation
protocol requires a class that adopts that protocol to implement the coordinate
property. In this case, it makes sense for Site
and Hotel
model objects to add the responsibilities of an annotation object to their bag of tricks. After all, the Site
and Hotel
model objects already know what thing you want to see or what hotel you're going to stay at, respectively. It makes sense for these objects to have the coordinate and callout data as well.
Of course, the only problem is that you haven't yet created those objects. In fact, you don't create them until Book VI, where I show you how to use plists and core data to access data and store objects, respectively.
So in this case, just as an illustration, I'm going to have you add an Annotation object just to get you into the rhythm.
Here's what you need to do to make the annotations thing happen:
As you did way back at the beginning of the "Building Your Map Functionality" section, go to the File
Add the code in Bold to MapAnnotation.h
.
#import <Foundation/Foundation.h>#import <MapKit/MapKit.h>
@interface MapAnnotation : NSObject<MKAnnotation>
{CLLocationCoordinate2D coordinate;
NSString *annotationTitle;
NSString *annotationSubTitle;
}
@property (nonatomic) CLLocationCoordinate2D
coordinate;
- (id) initWithTitle:(NSString*) title
subTitle: (NSString*) subTitle
coordinate:(CLLocationCoordinate2D) aCoordinate;
@end
Add the code in bold to MapAnnotation.m
.
#import "MapAnnotation.h" @implementation MapAnnotation@synthesize coordinate;
- (id) initWithTitle: (NSString*) title
subTitle:(NSString*) subTitle
coordinate:(CLLocationCoordinate2D)
aCoordinate {
if ((self = [super init])) {
coordinate = aCoordinate;
annotationTitle = title;
[annotationTitle retain];
annotationSubTitle = subTitle;
[annotationSubTitle retain];
}
return self;
}
-(NSString*) title {
return annotationTitle;
}
-(NSString*) subtitle {
return annotationSubTitle;
}
@end
What you did here was this:
Have the MapAnnotation
adopt the MKAnnotation
protocol.
@interface MapAnnotation: NSObject <MKAnnotation>
{
Add the following instance variable to the MapAnnotation.h
file.
CLLocationCoordinate2D coordinate;
Add the following property and method to the MapAnnotation.h
file.
@property (nonatomic) CLLocationCoordinate2D coordinate;
This is a requirement if you adopt the MKAnnotation
protocol and tells the MKMapView
where to place the annotation.
The MKAnnotation
protocol requires a coordinate
property — the title
method is optional.
Add a synthesize statement to the MapAnnotation.m
file.
@synthesize coordinate;
Implement the MapAnnotation title
and subtitle
methods by adding the following to the MapAnnotation.m
file:
-(NSString*) title { return annotationTitle; } -(NSString*) subtitle { return annotationSubTitle; }
Next, you need to add the following to Trip.m
to create the annotations. (You're doing it here because later — after you do all that stuff in Book VI — the code you have added here will make it easier to implement the "real" annotations).
- (NSArray*) createAnnotations { CLLocationCoordinate2D theCoordinate; theCoordinate.latitude = 37.774929; theCoordinate.longitude = −122.419415; MapAnnotation* sampleAnnotation = [[MapAnnotation alloc] initWithTitle: @"Sample annotation" subTitle: @"pretty easy" coordinate: theCoordinate]; NSMutableArray* annotations = [[NSMutableArray alloc] initWithCapacity:1]; [annotations addObject:sampleAnnotation]; return annotations; }
You can see that when I initialize the MapAnnotation
object I am setting a title (@"Sample annotation"
) and subtitle (@"pretty easy"
).
Trip
is creating a MapAnnotation
that will initialize its coordinate property with the latitude and longitude of San Francisco, which will be used by the Map view to position the annotation.
You will also need to import MapAnnotation
#import "MapAnnotation.h"
and add the method declaration to the Trip.h
file.
- (NSArray*) createAnnotations;
Finally, add the code in bold to the viewDidLoad:
method in MapController.m
so your code can send a message to Trip
to create the annotations.
- (void)viewDidLoad { [super viewDidLoad]; mapView.showsUserLocation = YES; CLLocationCoordinate2D initialCoordinate = [trip initialCoordinate]; [self updateRegionLatitude:initialCoordinate.latitude longitude:initialCoordinate.longitude latitudeDelta:.06 longitudeDelta:.06]; self.title = [trip mapTitle]; [mapView.userLocation addObserver:self forKeyPath:@"location" options:0 context:NULL];annotations = [[NSMutableArray alloc]
initWithCapacity:1];
[annotations addObjectsFromArray:
[trip createAnnotations]];
}
The MapController
object creates an array of annotation objects. (I'll show you how it's used next.) You'll also need to add the new annotations instance variable to MapController.h
:
NSMutableArray *annotations;
So far so good. MapAnnotation
has adopted the MKAnnotation
protocol, declared a coordinate
property, and implemented title
and subtitle
methods. The MapController
object then creates an array of these annotations (in this case one). The only thing left to do is send the array to the Map view to get the annotations displayed.
Displaying the annotations is easy. All you have to do is add the line of code in bold to the viewDidLoad
method in MapController.m
.
- (void)viewDidLoad {
[super viewDidLoad];
mapView.showsUserLocation = YES;
CLLocationCoordinate2D initialCoordinate =
[trip initialCoordinate];
[self updateRegionLatitude:initialCoordinate.latitude
longitude:initialCoordinate.longitude
latitudeDelta:.06 longitudeDelta:.06];
self.title = [trip mapTitle];
[mapView.userLocation addObserver:self
forKeyPath:@"location" options:0 context:NULL];
NSMutableArray* annotations = [[NSMutableArray alloc]
initWithCapacity:1];
[annotations addObjectsFromArray:
[trip createAnnotations]];
[mapView addAnnotations:annotations];
}
The MapController
sends the addAnnotations:
message to the Map view, passing it an array of objects that conform to the MKAnnotation
protocol; that is, each one has a coordinate
property and an optional title
(and subtitle
) method if you want to actually display something in the annotation callout.
The Map view places annotations on the screen by sending its delegate the mapView:viewForAnnotation:
message. This message is sent for each annotation object in the array. Here you can create a custom view or return nil
to use the default view. (If you don't implement this delegate method — which you won't, in this case — the default view is also used.
Creating your own Annotation views is beyond the scope of this book (although I will tell you that the most efficient way to provide the content for an Annotation view is to set its image property). Fortunately, the default Annotation view is fine for your purposes. It displays a pin in the location specified in the coordinate property of the Annotation delegate — MapAnnotation
in this case, and later Sight
and Hotel
), and when the user touches the pin, the optional title and subtitle text will display if the title
and subtitle
methods are implemented in the annotation delegate.
You can also add callouts to the Annotation callout, such as a Detail Disclosure button (the one that looks like a white chevron in a blue button in a table view cell), or the Info button (like the one you see in many of the utility apps) without creating your own Annotation view. Again, another exercise for you, if you're feeling frisky.
If you compile and build your project, you can check out one of the annotations you just added in Figure 5-8.
Remember I did this here simply to show you the mechanics of how it's done. You wouldn't normally have the annotation logic in a map controller. Instead, it would be in a model object, and I show you how to do that in Book VI.
Although you can pan to the user location on the map, in this case, it's kind of annoying, unless you're actually coding this at or around San Francisco. To remove at least that annoyance from your life, I want to show you how easy it is to add a button to the navigation bar to zoom you in to the current location and then back to the map region and span you're currently displaying.
Add the following code to add the button in the MapController
method viewDidLoad
.
You have quite a bit of code there, so this is just what to add:
UIBarButtonItem *locateButton = [[UIBarButtonItem alloc] initWithTitle: @"Locate" style:UIBarButtonItemStylePlain target:self action:@selector(goToLocation:)]; self.navigationItem.rightBarButtonItem = locateButton; [locateButton release];
This may look familiar, because it's what you did to add the Back button in Chapter 4 of this minibook. When the user taps the Locate button you create here, you've specified that the goToLocation:
message is to be sent (action:@selector(goToLocation:)
to the MapController
(target:self
).
Add the goToLocation:
method to MapController.m
.
- (IBAction)goToLocation:(id)sender { MKUserLocation *annotation = mapView.userLocation; CLLocation *location = annotation.location; if (nil == location) return;
CLLocationDistance distance = MAX(4*location.horizontalAccuracy,500); MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance (location.coordinate, distance, distance); [mapView setRegion:region animated:NO]; self.navigationItem.rightBarButtonItem.action = @selector(goToTrip:); self.navigationItem.rightBarButtonItem.title = @"Map"; }
When the user presses the Locate button, you first check to see if the location is available (it may take a few seconds after you start the application for the location to become available). If not, you simply return. (You could, of course, show an alert informing the user what is going on and try again in 10 seconds or so — I leave that up to you.)
If it's available, you compute the span for the region you'll be moving to. In this case, the code
CLLocationDistance distance = MAX(4*location.horizontalAccuracy,1000);
computes the span to be four times the horizontalAccuracy
of the device (but no less than 1,000 meters). horizontalAccuracy
is a radius of uncertainty given the accuracy of the device; that is, the user is somewhere within that circle.
You then call the MKCoordinateRegionMakeWithDistance
function that creates a new MKCoordinateRegion
from the specified coordinate and distance values. distance
and distance
correspond to latitudinalMeters
and longitudinalMeters
, respectively (I'm using the same value for both arguments here).
If you didn't want to change the span, you could have simply set the Map view's centerCoordinate
property to userLocation
, and, as I said earlier in the "It's about the region" section, that would have centered the region at the userLocation
coordinate without changing the span.
Change the title on the button to "Map," and change the @selector
to (goToTrip:
), which means that the next time the user touches the button, the goToTrip:
message will be sent, so you'd better add the following code. This will toggle the button title back and forth depending on which view you are in:
- (IBAction) goToTrip:(id)sender { CLLocationCoordinate2D initialCoordinate = [trip initialCoordinate]; [self updateRegionLatitude: initialCoordinate.latitude longitude:
initialCoordinate.longitude latitudeDelta:.06 longitudeDelta:.06]; self.navigationItem.rightBarButtonItem.title = @"Locate"; self.navigationItem.rightBarButtonItem.action = @selector(goToLocation:); }
Again add both method declarations to the MapController.h
file.
You can see the result of touching the Locate button in Figure 5-9.
Because you have the user location, you might be tempted to use that to center the map, and that would work fine, as long as you start the location-finding mechanism stuff as soon as the program launches. The problem is that, as I mention in Step 2 of the previous step list, the hardware may take a while to find the current location, and if you don't wait long enough, you get an error. You can add the code to center the map to a method that executes later, such as
-(void)observeValueForKeyPath:(NSString *) keyPath ofObject:(id)object change:(NSDictionary *) change context:(void *) context {
which gets called as soon as the map starts getting location information. But you will see an initial view, and then a redisplay of the centered view. For aesthetic reasons then, you really need to initialize the MapController
and MapView
at program start — an exercise for the reader.
18.118.184.223