iPhone Dev Sessions: Finding Your Way With MapKit
Looking for directions on how to create a simple map within an application can be challenging. Sometimes the simplest of typos or a missed step in the process can become very frustrating. Many of the examples start with a finished solution and attempt to explain the code after it was written. This walkthrough, from the first step, will attempt to dissect each challenge one by one in an effort to allow the reader to pick and choose which antidotes are applicable to their particular situation.
Creating an Application with a Map
For starters, create a new View-based Application and open the default .xib file that is created. All that is really needed to get the point across is a view controller. This example will utilize a .xib file and Interface Builder, but the interactions with Interface Builder are limited to establishing a reference and a delegate. If you prefer to do the example entirely in code, that should be just fine too. From within Interface Builder, drag and drop a Map View component from the Library Window onto the View. You may want it to be just this easy, but there are a few things that we need to do before exiting Interface Builder. Take a look at the Attributes of the Map View you just added in the Inspector Window:
The options are pretty straight forward to configure. Quit Interface Builder, but be sure to save all work before exiting. If you try to build and run the project at this point, it will fail. This is because the MapKit Framework has not jet been added to the project. From within Xcode, select the Frameworks folder and Add an Existing Framework to the Project. Now the project should build and execute properly. In the simulator, all that should display is a map of the world (assuming you have network connectivity on the Mac you are using to develop with).
Centering the Map
As a first step, let’s try and take control of the World by centering the Map on just the United States. As a first step in this process, one needs to know the actual latitude and longitude of the center of the United States. Breaking away from Google (who has defined the center of the U.S. elsewhere), we will use a small park near the town of Lebanon, Kansas since they have a historical monument declaring itself as the center of the country, rather than a spot just north of a country club in Coffeyville, Kansas. When working with a Map View, it wants to center on a region, more specifically a MKCoordinateRegion. Now this region in turn wants to know a location (or CLLocationCoordinate2D) and a span (or MKCoordinateSpan). Basically a region is initially configured with a center point and knowledge in coordinate terms as to how far to zoom in/out on the map. How far to zoom would define the span. The first step is ensuring that both that the MapKit and the CoreLocation frameworks have been added to the view controller’s header:
At this point it is time to establish the center of the United States by creating a location with the appropriate latitude and longitude.
CLLocationCoordinate2D location; location.latitude = 37.250556; location.longitude = -96.358333;
Now for the tricky part. The span object will need to know about the four corners of the area in question, not just the center. The furthest points north, south, east and west. Once these coordinates are known, it is a simple calculation to determine the span. Rather than using an exact number, pad the span by 4 percent in order to make a more natural looking map:
MKCoordinateSpan span; span.latitudeDelta = 1.04*(126.766667 - 66.95) ; span.longitudeDelta = 1.04*(49.384472 - 24.520833) ;
The center location is known and set, and the span is known and set. These are the requirements of the region:
MKCoordinateRegion region; region.span = span; region.center = location;
Then in Interface Builder, select the Map View Component, and from the Connections Tab inside the Inspector, create a New Referencing Outlet and assign it to the File’s Owner (which in this case is the View Controller). Quit and Save Interface Builder and run the application. In the header file, be sure to create an IBOutlet for the MapView, otherwise you will not have anything to wire up when using Interface Builder.
IBOutlet MKMapView *myMapView;
All that is left to do is to wire up the MapView that was created in Interface Builder with the View Controller, and set the region of the map View.
myMapView setRegion:region animated:NO; myMapView regionThatFits:region;
The continental United States should now be centered within the simulator. This technique can be used at any time to center the map within any region. All that is needed is the max and min latitude, the max and min longitude, and the center lat/lng coordinates. There are other means to control the span based on just knowing the center location. What I have found is that knowing the center is not always as easy as it sounds, especially when dealing with a list of locations. It is far easier to keep track of the furthest point north, south, east and west in an effort to determine the couners of the area in question.
Annotating a Map
Annotating a map is basically just adding pins to it. This can occur programmatically by accessing data that has been stored locally or is accessed via a remote API, or can be the result of getting location information from either the device in the form or Core Locations API or by data entered by the user like an address. In the end, in order to get it to show up on a map (however the data is collected, stored or referenced), it must conform to the MKAnnotation protocol. The only required property of an object that adopts this protocol is a CLLocationCoordinate2D of the name coordinate:
@property (nonatomic, readonly) CLLocationCoordinate2D coordinate
There are also two optional instance methods that can be implemented. These are not required to be added to a map, but are necessary if one wants to support callouts on the pins that display on the map. These two methods return strings and are named title and subtitle:
- (NSString *)title - (NSString *)subtitle
Keep in mind that since you are not subclassing, any object can adopt this protocol and be added to a map as an annotation. In its simplest form, an implementation of an annotation would have at least the following declared in its header:
#import #import @interface AdoptingAnAnnotation : NSObject @property (nonatomic, readonly) CLLocationCoordinate2D coordinate; - (NSString *) title; - (NSString *) subtitle; @end
What is great about the MKAnnotation protocol is that any class can adopt it. This means that if you already have a model object that has address information contained within it, you could adopt the MKAnnotation protocol in the existing class and delegate access to information already contained within that class to the optional title and subtitle accessor methods. Keeping the example simple, the following is the basics that are required to support the MKAnnotation protocol in a simple to reuse implementation:
#import "AdoptingAnAnnotation.h" @implementation AdoptingAnAnnotation @synthesize latitude; @synthesize longitude; - (id) initWithLatitude:(CLLocationDegrees) lat longitude:(CLLocationDegrees) lng latitude = lat; longitude = lng; return self; - (CLLocationCoordinate2D) coordinate CLLocationCoordinate2D coord = self.latitude, self.longitude; return coord; } - (NSString *) title return @"217 2nd St"; - (NSString *) subtitle return @"San Francisco CA 94105"; @end
Thats it! Depending on how you want to have annotations added to the map, this could be behind the scenes by parsing through the result of a network call that returns data that must be parsed through, or by using CoreLocations and adding a button to the user interface to add annotations each time the button is clicked. Using the simple implementation of a class that adopted the MKAnnotaiton protocol above, the following code would could be added anywhere within the ViewController:
AdoptingAnAnnotation *someAnnotation = AdoptingAnAnnotation alloc initWithLatitude:37.786521 longitude:-122.397850 ] autorelease]; mMapView addAnnotation:someAnnotation;
Default red pins are fine, but you may want to use custom annotations as well. The simplest form of customizing the annotations is to simply change the color of the pin. So we get to the main event, a means to display the annotated class on the map that we have added to the view. The first step is to decide which class will be the delegate to the MKMapView that was added to the View. Typically this is the ViewController that the MapView was added to. This can be done in Interface Builder by making the ViewController the delegate to the MapView, or in code by setting the delegate of the MapView to self in one of the init methods of the View Controller. Once this is complete, it is assumed that the View Controller has adopted the MKMapViewDelegate protocol. Once the header of the View Controller has been updated to indicate this fact, all that is required is that the following method is implemented in the View Controller class:
- (MKAnnotationView *) mapView:(MKMapView *) mapView viewForAnnotation:(id ) annotation MKPinAnnotationView *customPinView = MKPinAnnotationView alloc initWithAnnotation:annotation reuseIdentifier:nil] autorelease]; customPinView.pinColor = MKPinAnnotationColorPurple; customPinView.animatesDrop = YES; customPinView.canShowCallout = YES; return customPinView;
In this way, one has control over some of the basic aspects of the pin annotations that are added to the mapView. If you find that the above code does not work for you, check and double-check that the delegate for the map view has been set up properly. This can be done in Interface Builder or in code as follows:
Taking control of the Pins by implementing the mapView method in the MKMapViewDelegate protocol is a good start, but most want to abandon pins all together and utilize custom images on the MapView instead. The technique is identical, but the difference is in the implementation:
- (MKAnnotationView *) mapView:(MKMapView *) mapView viewForAnnotation:(id ) annotation MKAnnotationView *customAnnotationView=MKAnnotationView alloc initWithAnnotation:annotation reuseIdentifier:nil] autorelease]; UIImage *pinImage = UIImage imageNamed:@"ReplacementPinImage.png"; customAnnotationView setImage:pinImage; customAnnotationView.canShowCallout = YES; return customAnnotationView;
The difference is that in this situation a MKAnnotationView is used instead of the MKPinAnnotation class. Try as you might, adding images to pins will not work. Setting a MKAnnotations’s image property will achieve the desired results. This is where several online code examples can be misleading by using the same reference name like pinView or annoView and only change the Class in the alloc statement. And since this is all in the MapKit framework, the imports do not need to change. Therefore this one subtle difference is often the culprit of several hours of pain and suffering.
NOTE: It is recommended that one resize the image prior to compilation using a tool like preview rather than attempt to resize the image in code.
As we saw earlier, annotations can have titles and subtitles. These are optionally displayed when the user taps on a given pin or custom annotation. In the prior code examples, we set the property of ShowCallout to YES. These callouts can also have images associated with them. These images are not meant to be the same image that was utilized as the pin image as these will be displayed in the callout above the pin when the pin receives a tap event. This technique will expand upon the one utilized in either of the two prior examples by adding the following two lines of code inside of the MKAnnotationView delegate method:
UIImageView *leftIconView = UIImageView alloc initWithImage:UIImage imageNamed:@"LeftIconImage.png"]; customAnnotationView.leftCalloutAccessoryView = leftIconView;
You’re not required to use the MKAnnotationView in order to add an image to the left callout accessory view. This can also be done with the MKPinAnnotationView.
NOTE: It is recommended that you resize the image prior to compilation using a tool like Preview rather than attempt to resize the image in code.
Assigning Actions to Annotation Callouts
Finally there is the matter of assigning an action to an annotation’s callout. Typically, these actions pop another view onto the stack, but really can do anything. What is required here is that a button be added to the AnnotationView (either the MKPinAnnotiaonView or the MKAnnotationView example above). This button will assign one of its control events to a method typically in the same ViewController that is also the delegate to the MapView itself:
UIButton *rightButton = UIButton buttonWithType:UIButtonTypeDetailDisclosure; rightButton addTarget:self action:@selector(annotationViewClick:) forControlEvents:UIControlEventTouchUpInside; customAnnotationView.rightCalloutAccessoryView = rightButton;
Just as in the prior example, this code should be added to the delegate method implemented in the View controller. For example, if all of the details were added to the MKAnnotationView, the code would look as follows:
- (MKAnnotationView *) mapView:(MKMapView *) mapView viewForAnnotation:(id ) annotation MKAnnotationView *customAnnotationView=MKAnnotationView alloc initWithAnnotation:annotation reuseIdentifier:nil] autorelease]; UIImage *pinImage = UIImage imageNamed:@"ReplacementPinImage.png"; customAnnotationView setImage:pinImage; customAnnotationView.canShowCallout = YES; UIImageView *leftIconView = UIImageView alloc initWithImage:UIImage imageNamed:@"LeftIconImage.png"]; customAnnotationView.leftCalloutAccessoryView = leftIconView; UIButton *rightButton = UIButton buttonWithType:UIButtonTypeDetailDisclosure; rightButton addTarget:self action:@selector(annotationViewClick:) forControlEvents:UIControlEventTouchUpInside; customAnnotationView.rightCalloutAccessoryView = rightButton; return customAnnotationView; - (IBAction) annotationViewClick:(id) sender NSLog(@"clicked");
These are the basic building blocks that are used when working with maps on the iPhone. These building blocks can be used in isolation, or can be added together to work with each other in concert. For instance, when adding an annotation to a map, the map can be re-centered taking the location new annotation into account. While these bare bones examples are not a true representation of a real world application, they were meant to illustrate the basics in a very minimalist approach that simply works.
The rest is here:
iPhone Dev Sessions: Finding Your Way With MapKit