Chapter 5. Finding Your Way

In This Chapter

  • 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.

Note

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.)

Building Your Map Functionality

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.

  1. In the RoadTrip Project window, select the Classes folder and then choose File

    Building Your Map Functionality
  2. 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.

  3. Enter MapController.m in the File Name field and then click Finish.

  4. Choose File

    Building Your Map Functionality
  5. 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.

  6. 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.)

  1. Click the disclosure triangle next to Targets in the Groups & Files list and then right-click on RoadTrip.

    Warning

    Be sure to do this using the Targets folder, or Step 3 won't work!

  2. From the submenu that appears, select Add and then select Existing Frameworks, as I've done in Figure 5-1.

  3. 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.

    Adding a new framework.

    Figure 5-1. Adding a new framework.

    Adding the MapKit.framework.

    Figure 5-2. Adding the MapKit.framework.

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.

  1. 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"
  2. 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 > {
  3. 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;
  4. 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;
  5. 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;
  6. 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.

  7. 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"
  8. Do the File

    Adding the MapKit.framework.

    Note

    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.

Setting up 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.

Note

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.

Default Map view — you get a map for free.

Figure 5-3. Default Map view — you get a map for free.

  1. 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.

    Tip

    If the Attributes Inspector window is not open, choose Tools

    Default Map view — you get a map for free.

    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

    Default Map view — you get a map for free.
  2. 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.

  3. Click in the View window and then choose MKMapView from the Class drop-down menu in the Identity Inspector.

    Note

    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.

  4. 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.

  5. 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.

  6. 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.

  7. With the cursor still in the Map View window, let go of the mouse button.

    A pop-up menu appears.

  8. Choose Delegate from the pop-up menu.

  9. Do the File

    Default Map view — you get a map for free.

    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.

    Note

    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.

San Francisco sites and where to stay.

Figure 5-4. San Francisco sites and where to stay.

Putting MapKit through Its Paces

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.

MKMapView

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.

Note

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.)

Enhancing the map

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.

Adding landscape mode and the current location

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

Adding landscape mode and the current location

Tip

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.

Note

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.

Displaying a map in landscape mode with a user location.

Figure 5-5. Displaying a map in landscape mode with a user location.

It's about the region

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).

  1. region.center.latitude specifies the latitude of the center of the map.

  2. 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.

  3. 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.

  4. 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];
}
How regions work.

Figure 5-6. How regions work.

Take a look at what adding the bold stuff does:

  1. 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).

  2. 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.

Warning

You should get four compiler warnings because you haven't yet implemented the MKReverseGeocoderDelegate protocol. Don't worry, what you'll do in the next few sections will eliminate them.

Regions determine what you see on the map.

Figure 5-7. Regions determine what you see on the map.

Dealing with failure

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];
}

Tip

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.

Adding annotations

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.

The annotation

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:

  1. As you did way back at the beginning of the "Building Your Map Functionality" section, go to the File

    The annotation
  2. 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
  3. 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:

  1. Have the MapAnnotation adopt the MKAnnotation protocol.

    @interface MapAnnotation: NSObject <MKAnnotation>  {
  2. Add the following instance variable to the MapAnnotation.h file.

    CLLocationCoordinate2D coordinate;
  3. 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.

    Note

    The MKAnnotation protocol requires a coordinate property — the title method is optional.

  4. Add a synthesize statement to the MapAnnotation.m file.

    @synthesize coordinate;
  5. Implement the MapAnnotation title and subtitle methods by adding the following to the MapAnnotation.m file:

    -(NSString*) title {
    
      return annotationTitle;
    }
    
    -(NSString*) subtitle {
    
      return annotationSubTitle;
    }
  6. 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

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.

Note

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.

An annotation.

Figure 5-8. An annotation.

Warning

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.

Going to the current location

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.

  1. 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).

  2. 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.

  3. 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:);
    }
  4. Again add both method declarations to the MapController.h file.

You can see the result of touching the Locate button in Figure 5-9.

Go to the current location.

Figure 5-9. Go to the current location.

Warning

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.

And There's Even More

You've covered a lot of ground in this chapter, and the map is looking pretty good. But there's another map topic you need to get under your belt: geocoding. I cover that bit of business in the next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.183.152