Using a Map Object
You added a MKMapView
to your user interface and configured it to show the current location
of the device. If you attempted to build and run the app, however,
you’ll have noticed that it fails immediately. The reason for this is
that as soon as you add the map view, you need to add a framework to
support it. For our example application, we also need to add two
frameworks to the project: Core Location, which deals with locations;
and Map Kit, which displays the embedded Google Map.
Adding the Location and Map Frameworks
In the Xcode project window,
right-click the Frameworks group and choose Add, Existing Frameworks
from the menu. In the scrolling list that appears, choose the MapKit.framework and CoreLocation.framework items, and then click Add.
Drag the frameworks into the Frameworks group if they don’t appear there automatically.
Next, edit the
BestFriendViewController.h interface file so that we have access to the
classes and methods in these frameworks. Add these lines after the
existing #import lines:
#import <CoreLocation/CoreLocation.h>
#import <MapKit/MapKit.h>
We can now display the MKMapView, work with locations and programmatically control the map.
Controlling the Map Display
Because we already get the display of the map and the user’s current location for “free” with the MKMapView, the only thing we really need to do in this application is take the user’s ZIP code, determine a latitude and longitude for it, and then center and zoom the map on that location.
Unfortunately, neither
Map Kit nor Core Location provides the ability to turn an address into a
set of coordinates, but Google offers a service that does. By
requesting the URL http://maps.google.com/maps/geo?output=csv&q=<address>
we get back a comma-separated list where the third and fourth values
are latitude and longitude, respectively. The address that we send to
Google is very flexible—it can be city, state, ZIP, street—whatever
information we provide, Google will try to translate it to coordinates.
In the case of a ZIP code, it displays the center of the ZIP code’s
region on the map—exactly what we want.
Once we have the location, we need to use center and zoom the map. We do this by defining a map “region,” and then using the setRegion:animated method. A region is a simple structure (not an object) called a MKCoordinateRegion. It has members called center, which is another structure called a CLLocationCoordinate2D (containing latitude and longitude); and span,
which denotes note many degrees to the east, west, north, and south of
the center are displayed. A degree of latitude is 69 miles. A degree of
longitude, at the equator, is 69 miles. By choosing small values for the
span within the region (like 0.2), we narrow our display down to a few
miles around the center point. For example, if we wanted to define a
region centered at 40.0 degrees latitude and 40.0 degrees longitude with
a span of 0.2 degrees in each direction, we could write the following:
MKCoordinateRegion mapRegion;
mapRegion.center.latitude=40.0;
mapRegion.center.longitude=40.0;
mapRegion.span.latitudeDelta=0.2;
mapRegion.span.longitudeDelta=0.2;
To center and zoom in on this region in a map object called map, we’d use the following:
[map setRegion:mapRegion animated:YES];
To keep things nice and neat in our application, we’re going to implement all of this functionality in a nice new method called centerMap:showAddress. centerMap:showAddress will take a two inputs: a string, zipCode, (a ZIP code), and a dictionary, fullAddress
(an address dictionary returned from the Address Book). The ZIP code
will be used to retrieve the latitude and longitude from Google, and
then adjust our map object to display it. The address dictionary will be used by the annotation view to show a callout from the annotation pushpin.
Enter the new centerMap method shown in Listing 5 within the BestFriendViewController implementation file.
Listing 5.
1: - (void)centerMap:(NSString*)zipCode showAddress:(NSDictionary*)fullAddress {
2: NSString *queryURL;
3: NSString *queryResults;
4: NSArray *queryData;
5: double latitude;
6: double longitude;
7: MKCoordinateRegion mapRegion;
8:
9: queryURL = [[NSString alloc]
10: initWithFormat:
11: @"http://maps.google.com/maps/geo?output=csv&q=%@",
12: zipCode];
13:
14: queryResults = [[NSString alloc] initWithContentsOfURL:
15: [NSURL URLWithString:queryURL]
16: encoding: NSUTF8StringEncoding
17: error: nil];
18: // Autoreleased
19: queryData = [queryResults componentsSeparatedByString:@","];
20:
21: if([queryData count]==4) {
22: latitude=[[queryData objectAtIndex:2] doubleValue];
23: longitude=[[queryData objectAtIndex:3] doubleValue];
24: // CLLocationCoordinate2D;
25: mapRegion.center.latitude=latitude;
26: mapRegion.center.longitude=longitude;
27: mapRegion.span.latitudeDelta=0.2;
28: mapRegion.span.longitudeDelta=0.2;
29: [map setRegion:mapRegion animated:YES];
30:
31: if (zipAnnotation!=nil) {
32: [map removeAnnotation: zipAnnotation];
33: }
34: zipAnnotation = [[MKPlacemark alloc]
35: initWithCoordinate:mapRegion.center addressDictionary:fullAddress];
36: [map addAnnotation:zipAnnotation];
37: [zipAnnotation release];
38: }
39:
40: [queryURL release];
41: [queryResults release];
42:
43: }
|
Let’s explore how this works. We kick things off in lines 2–7 by declaring several variables we’ll be needing: queryURL, queryResults, and queryData
will hold the Google URL we need to request, the raw results of the
request, and the parsed data, respectively. The latitude and longitude
variables are double-precision floating-point numbers that will be used
to store the coordinate information gleaned from queryData. The last variable, mapRegion, will be the properly formatted region that the map should display.
Lines 9–12 allocate and initialize queryURL with the Google URL, substituting in the zipCode string that was passed to the method. Lines 14–17 use the NSString method initWithContentsOfURL:encoding:error to create a new string that contains the data located at the location defined in queryURL. We also make use of the NSURL method URLWithString: to turn the queryURL string into a proper URL object. Any errors are disregarded.
Did you Know?
The initWithContentsOfURL:encoding:error
method expects an encoding type. The encoding is the manner in which
the string passed to the remote server is formatted. For almost all web
services, you’ll want to use NSUTF8StringEncoding.
Line 19 uses the NSString method componentsSeparatedByString, which takes a string, a delimiter character, and returns an NSArray that breaks apart the string based on the delimiter. Google is going to hand back data that looks like this: <number>,<number>,<latitude>,<longitude>. By invoking this method on the data using a comma delimiter (,), we get an array, queryData, where the third element contains the latitude and the fourth, the longitude.
Line 21 does a very
basic sanity check on the information we receive. If there are exactly
four pieces of information found, we can assume the results are valid
and lines 22–29 are executed.
Lines 22 and 23 retrieve the strings at indices 2 and 3 of the queryData array and convert them to double-precision floating-point values, storing them in the latitude and longitude variables.
By the Way
Remember, an array’s index starts at 0. We use an index of 2 to access the third piece of data in the array and an index of 3 to access the fourth.
Lines 25–29 define the region of the map to display and then uses setRegion:animated to redraw the map accordingly.
Finally, lines 31–38 handle
the annotation. In lines 31–33, we check to see whether an annotation
has already been allocated. (This will happen if the person using the
app chooses multiple addresses, resulting in the map being redrawn.) If zipAnnotation has already been used, we can call the MKMapView method removeAnnotation
to remove the existing annotation. Once removed, we are free to add a
new annotation to the map. Lines 34–35 allocate a new place mark, MKPlaceMark, using the point defined by the map object’s center property and described by the address dictionary passed to the method, fullAddress.
With the zipAnnotation placemark defined, we can add it to the map using addAnnotation method in line 36, and then release the placemark object in line 37.
Creating a Pin Annotation View
As it stands now,
your map implementation will work, but it really shouldn’t! For the
annotation to be displayed, we need to create an instance of an MKAnnotationView. As mentioned earlier, Apple provides a subclass of the MKAnnotationView called MKPinAnnotationView. When you call addAnnotation on the map view object, iOS is automatically creating an instance of the MKPinAnnotationView for you. Technically, however, we’re supposed to do this ourselves in map view’s delegate method mapView:viewForAnnotation. To keep things legit, add the following method to the BestFriendViewController.m implementation file:
1: - (MKAnnotationView *)mapView:(MKMapView *)mapView
2: viewForAnnotation:(id <MKAnnotation>)annotation {
3: MKPinAnnotationView *pinDrop=[[MKPinAnnotationView alloc]
4: initWithAnnotation:annotation reuseIdentifier:@"citycenter"];
5: pinDrop.animatesDrop=YES;
6: pinDrop.canShowCallout=YES;
7: pinDrop.pinColor=MKPinAnnotationColorPurple;
8: return pinDrop;
9: }
Lines 1–2 define the mapView:viewForAnnotation method as provided by Apple; this code shouldn’t change. Line 3 declares, allocates, and initializes an instance of MKPinAnnotationView using the annotation parameter that iOS sends to the mapView:viewForAnnotation method, along with a reuseIdentifier
string. This reuse identifier is a unique identifying string that
allows an allocated annotation to be reused in other places. For our
purposes, this could be any string you want.
The new pin annotation view, pinDrop, is configured through three properties in lines 5–7. The animatesDrop Boolean property, when true, animates the pin dropping onto the map. The canShowCallout property sets the pin so that it will display additional information in a callout when touched, and, finally, the pinColor, sets the color of the onscreen pin graphic.
Once properly configured, the new pin annotation view is returned to the map view in line 8.
Tying the Map Display to the Address Book Selection
Congratulations! Your code
now has the smarts needed to locate a ZIP code on the map, zoom in, and
add a pin annotation view! The last piece of magic we need to finish the
mapping is to hook it into the address book selection so that the map
is centered when a user picks a contact with an address.
Edit the peoplePickerNavigationController:shouldContinueAfterSelectingPerson method, adding the following line
[self centerMap:friendZip showAddress:friendFirstAddress];
immediately following this line:
friendZip = [friendFirstAddress objectForKey:@"ZIP"];
Our
application is nearing completion. All that remains is adding the
ability to send email to our chosen buddy. Let the implementation begin!