The Map Kit framework is an incredibly powerful and useful toolkit that adds immense functionality to the location services that " devices offer. The framework's key focus is the ability to place a user-interactive map in an application, with countless other features expanding functionality, allowing for a nearly entirely customizable mapping interface. iOS 5 has continued to improve the capabilities of Map Kit, improving developer capability and making map-based applications increasingly dynamic and useful.
For all projects in this chapter, as in all other chapters, make sure that ARC (Automatic Reference Counting) is enabled.
The core foundation of any Map Kit application is the actual displaying of the world map. In this section, you will go over how to create a Map Kit application, display a map, and allow the map to show the user's location.
You will start by creating a new project using the Single View Application template, as shown by Figure 5–1.
Figure 5–2 shows the configuration settings you will use for this project. Your version of Xcode may also include a box for Use Automatic Reference Counting. Make sure this box is always checked. You can name the project Chapter5Recipe1.
To begin, you will need to add the Map Kit framework and the Core Location framework to the project. In the navigator pane, select the Chapter5Recipe1 project file, and then make sure the Chapter5Recipe1 target is selected in the Editor view (if not already selected). Click the Build Phases tab, and expand the Link Binary With Libraries section. Click the + button to add the frameworks to the labels, as highlighted in Figure 5–3, and use the resulting pop-up resembling Figure 5–4 to add the required frameworks.
Now that the frameworks have been added, you can start to build your interface. Select the view controller's .xib
file from the navigation pane, and drag a MKMapView
from the objects browser to the workspace so that it fills the view.
Next, add a UILabel
on top of the MKMapView
that will be used to display the device's latitude and longitude. Figure 5–5 shows an example of what your user interface will resemble.
Using the Assistant Editor, drag a connection from the MKMapView
to the SBViewController
interface file (.h
) with a ^-click-drag from the MKMapView
to the SBViewController
file, as demonstrated in Figures 5–6 and 5–7.
NOTE: If the interface file is not shown in the second pane of the Assistant Editor, make sure you click your view controller in the workspace.
Name the MKMapView
outlet “mapViewUserMap”.
Repeat the same steps with the UILabel
, and name the outlet “labelUserLocation”.
Your user interface is fully set up, so you can simply focus on the interface file (.h
) now. Select the SBViewController.h
file in the navigation pane. There are two additions you need to make to this class interface before moving to the implementation file. The first is to add the MapKit/MapKit.h
framework library to the class with an import statement, and the second is to define the class as complying with the MKMapViewDelegate
protocol. Apple recommends that when you use a MapView
, you should assign it a delegate object. The completed interface file (.h
) looks like this:
// SBViewController.h
// Chapter5Recipe1
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
@interface SBViewController : UIViewController <MKMapViewDelegate> {
MKMapView *mapViewUserMap;
UILabel *labelUserLocation;
}
@property (strong, nonatomic) IBOutlet MKMapView *mapViewUserMap;
@property (strong, nonatomic) IBOutlet UILabel *labelUserLocation;
@end
Switch to the implementation file, SBViewController.m
, and let's start by setting up the MKMapView
in the viewDidLoad
method. Whenever you use an MKMapView
object, you should set its delegate and its region.
The region is the portion of the map that is currently being displayed. The region consists of a center coordinate and a distance in latitude and longitude to show surrounding the center coordinate. If you are like most people, you don't think of distances in latitudinal and longitudinal degrees, so you can use the method MKCoordinateRegionMakeWithDistance
to create a region using a center coordinate and meters surrounding the coordinate. If you are unfamiliar with the metric system, a meter is just a tiny bit longer than a yard.
In this recipe, I chose to start with the map initially panned to show my hometown of Baltimore, MD. I define a coordinate for this location and then define a region that contains the area 10km x 10km around this center coordinate.
//Set MKMapView delegate
self.mapViewUserMap.delegate=self;
//Set MKMapView starting region
CLLocationCoordinate2D coordinateBaltimore = CLLocationCoordinate2DMake(39.303, -
76.612);
self.mapViewUserMap.region=
MKCoordinateRegionMakeWithDistance(coordinateBaltimore,
10000,
10000);
Two optional properties worth mentioning are .zoomEnabled
and .scrollEnabled
. These two properties control the interactions a user can have with the map. They can prevent a user from zooming or panning a map, respectively.
//Optional Controls
// self.mapViewUserMap.zoomEnabled=NO;
// self.mapViewUserMap.scrollEnabled=NO;
Finally, you will define the map as showing the user's location. This is easily done with the .showUserLocation
property. Setting this property to YES will start the Core Location tracking methods and prompt the user to authorize location tracking for this application.
NOTE: Just because showUserLocation
is set to YES, the user's location is not automatically visible on the map. To determine if the location is visible in the current region of the map, use the property userLocationVisible
.
After you have told the map that you want to show the user location, you can also tell the map to track the user location by setting the .userTrackingMode
property or using the method setUserTrackingMode:animated:
. This property accepts three possible values:
MKUserTrackingModeNone
: Does not track the user's location; the map can be moved to a region that does not contain the user's location.MKUserTrackingModeFollow
: Map will be panned to keep the user's location at the center. The top of the map will be North. If the user pans the map manually, tracking will stop.MKUserTrackingModeFollowWithHeading
: Map will be panned to keep the user's location at the center, and the map will be rotated so that the user's heading is at the top of the map. If the user pans the map manually, tracking will stop.Initially, you are going to set userTrackingMode
to MKUserTrackingModeFollow
, but later I will show how to give users the ability to control the tracking mode themselves. The following if
statement will confirm that location services have already been enabled on the device.
//Control User Location on Map
if ([CLLocationManager locationServicesEnabled])
{
mapViewUserMap.showsUserLocation = YES;
[mapViewUserMap setUserTrackingMode:MKUserTrackingModeFollow animated:YES];
}
In whole, your viewDidLoad
method looks like the following:
- (void)viewDidLoad
{
[super viewDidLoad];
//Set MKMapView delegate
self.mapViewUserMap.delegate=self;
//Set MKMapView starting region
CLLocationCoordinate2D coordinateBaltimore = CLLocationCoordinate2DMake(39.303, -
76.612);
self.mapViewUserMap.region=
MKCoordinateRegionMakeWithDistance(coordinateBaltimore,
10000,
10000);
//Optional Controls
// self.mapViewUserMap.zoomEnabled=NO;
// self.mapViewUserMap.scrollEnabled=NO;
//Control User Location on Map
if ([CLLocationManager locationServicesEnabled])
{
mapViewUserMap.showsUserLocation = YES;
[mapViewUserMap setUserTrackingMode:MKUserTrackingModeFollow animated:YES];
}
}
The next important thing is the viewDidUnload
method. Whenever you set a delegate for an object, you should set the delegate = nil before you release the object in order to avoid any memory issues. So you'll need to add a line in your viewDidUnload
to set the delegate to nil on self.mapViewUserMap
:
- (void)viewDidUnload
{
self.mapViewUserMap.delegate=nil;
[self setMapViewUserMap:nil];
[self setLabelUserLocation:nil];
[super viewDidUnload];
}
The last thing you will do is set up one of the mapView
delegate methods to update the label with the user's current location. You will use the -mapView:didUpdateUserLocation:
delegate method. Your implementation of the method will look like this:
-(void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation
*)userLocation{
self.labelUserLocation.text=
[NSString
stringWithFormat:@"Current Location: %.5f°, %.5f°",
userLocation.coordinate.latitude,
userLocation.coordinate.longitude];
}
You have enough of a start that you can now run your app on the simulator. You can run the app by using the keyboard shortcut ⌘R. When the app launches on the simulator, the user will be prompted to allow the app access to his or her location. Figure 5–8 shows your application displaying this exact prompt.
If you click OK and you are looking at the city of Baltimore on your device (and there is no sign of your location on the map), then you may need to start the location debug services. On the simulator, go to the menu option Debug Location Freeway Drive, and this will start the location simulation services on the simulator. The map should pan to the new location (a drive recorded in California) and update the location label.
One of the problems users will experience with this recipe is if they try to manually pan the map, the user location tracking stops. Apple has provided a new UIBarButtonItem
class named MKUserTrackingBarButtonItem
. This button can be added to any UIToolBar
or UINavigationbar
and will toggle the user tracking modes on the specified map view.
You initialize the MKUserTrackingBarButtonItem
by passing it the MKMapView
that you want it to control with the initWithMapView:
method.
To set this up, you'll add a UIToolbar
to your .xib
file in Interface Builder and create an outlet for it named “toolbarMapTools”. You can delete the default BarButtonItem
it adds to the toolbar or your viewDidLoad
method will override it. If you delete it from the .xib
file, your user interface will now resemble Figure 5–9.
Now you will add your MKUserTrackingBarButtonItem
in code. Switch to the view controller implementation file, SBViewController.m
, and scroll to the viewDidLoad
method. Add the following code at the bottom of viewDidLoad
:
//Create BarButtonItem for controller user location tracking
MKUserTrackingBarButtonItem *trackingBarButton =
[[MKUserTrackingBarButtonItem alloc] initWithMapView:self.mapViewUserMap];
//Add UserTrackingBarButtonItem to UIToolbar
[self.toolbarMapTools
setItems:[NSArray arrayWithObject:trackingBarButton]
animated:YES];
With this new add-on, users can manually pan the map and then get back to tracking their location with the push of this new bar button. Figure 5–10 demonstrates the user-tracking functionality you have implemented.
NOTE: MKUserLocationFollowWithHeading
is not functional in the iOS Simulator.
Often, one of the most useful things about having a map is to see not only where the user is, but also where whatever the user is looking for is as well. To do this, you add annotations to your map. This recipe will build on top of the previous recipe.
First, you need to define your annotations by creating a subclass of NSObject
. To do this, go to File New New File, and under the Cocoa Touch category, choose “Objective-C class,” as in Figure 5–11.
Name your class something clear like “MyAnnotation”, and make sure it is a subclass of NSObject
, as shown in Figure 5–12.
Click Next and then Create to add the class to your project.
The next thing to do is set up your MyAnnotation
to conform to the MKAnnotation
protocol. This will require an import statement for <MapKit/MapKit.h>
in your header file, as well as a required coordinate
property. You will also be implementing the optional title
and subtitle
properties, and creating an initWithCoordinate
method. The code to do this is as follows:
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
@interface MyAnnotation : NSObject <MKAnnotation>
{
NSString *title;
NSString *subtitle;
}
@property (nonatomic) CLLocationCoordinate2D coordinate;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subtitle;
-(id) initWithCoordinate:(CLLocationCoordinate2D) aCoordinate;
@end
Make sure to synthesize these three properties in your MyAnnotation.m
implementation file. This file will read like so:
#import "MyAnnotation.h"
@implementation MyAnnotation
@synthesize coordinate, title, subtitle;
-(id) initWithCoordinate:(CLLocationCoordinate2D) aCoordinate
{
self=[super init];
if (self){
coordinate = aCoordinate;
}
return self;
}
@end
Now you need to define how your MKMapView
deals with annotations back in your view controller's implementation file. To do this, you will implement the following - mapView:viewForAnnotation:
method. This method is quite similar to that used to make views for TableView
cells. Be sure to import your MyAnnotation.h
file into your view controller, or this will not compile.
- (MKAnnotationView *)mapView:(MKMapView *)mapView
viewForAnnotation:(id<MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MyAnnotation class]]) //Ensures the User's location
is not affected.
{
static NSString *annotationIdentifier=@"annotationIdentifier";
//Try to get an unused annotation, similar to uitableviewcells
MKAnnotationView *annotationView=[self.mapViewUserMap
dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];
annotationView.annotation = annotation;
//If one isn't available, create a new one
if(!annotationView)
{
annotationView=[[MKPinAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:annotationIdentifier];
}
//Optional properties to change
annotationView.canShowCallout = YES;
annotationView.rightCalloutAccessoryView = [UIButton
buttonWithType:UIButtonTypeDetailDisclosure]; //Creates button on right of callout
return annotationView;
}
return nil;
}
Your two optional properties, canShowCallout
and rightCalloutAccessoryView
, are very useful for making interactive maps. The first causes a small callout to pop up if a pin is pressed, and the second changes the appearance of the right side of the callout. By default, the text in the callout will be the title and subtitle of the annotation, meaning that if you intend to show callouts, your annotations should have at least a title.
Finally, the last thing to do is to create your annotations with their information and add them to the map. You create a mutable array to store your annotations, and then add the array of annotations to your mapView
, as follows. This code goes in your viewDidLoad
method.
//Create and add Annotations
NSMutableArray *annotations = [[NSMutableArray alloc] initWithCapacity:2];
MyAnnotation *ann1 = [[MyAnnotation alloc]
initWithCoordinate:CLLocationCoordinate2DMake(25.802, -80.132)];
ann1.title = @"Miami";
ann1.subtitle = @"Annotation1";
MyAnnotation *ann2 = [[MyAnnotation alloc]
initWithCoordinate:CLLocationCoordinate2DMake(39.733, -105.018)];
ann2.title = @"Denver";
ann2.subtitle = @"Annotation2";
[annotations addObject:ann1];
[annotations addObject:ann2];
[self.mapViewUserMap addAnnotations:annotations];
I chose to make your pins drop in Miami and Denver, but any coordinates will work just as well. If you run this app now, you should see your normal map, but with a couple of pins stuck in, as in Figure 5–13. You will probably need to zoom out in order to see them; this can be done in the simulator by holding , to simulate a pinch, and dragging.
Oftentimes it may be useful to have an application in which a map's annotations are moveable by dragging them across the map. Implementing this functionality is incredibly simple, and can be done by adding a single line to your -viewForAnnotation:
delegate method:
annotationView.draggable = YES;
As long as you have synthesized your annotation's coordinate
property, your pins (or any other AnnotationView
you decide to use) will be draggable.
While the majority of the time the default MKPinAnnotationView
objects are incredibly useful, you may at some point decide you want a different image instead of the pin to represent an annotation on your map. This could be anything from an image of your friend representing his or her hometown, to your logo representing your company's location. In order to create a custom annotation view, you will be subclassing the MKAnnotationView
class. You will also be customizing your callouts to display more than simply a title and subtitle.
First, you must create your project the same way as earlier in this chapter, naming it “customAnnotationViews
”, and add your Map Kit and Core Location frameworks. You will then add your Map Kit to your view controller, making sure to use your #import
statements for both <MapKit/MapKit.h>
and <CoreLocation/CoreLocation.h>
, and setting your view controller to conform to the MKMapViewDelegate
protocol (refer to the beginning of this chapter on how to perform these tasks). You must also not forget to set your MapView
's delegate to your view controller, by modifying your -viewDidLoad
method like so:
- (void)viewDidLoad
{
[super viewDidLoad];
self.mapViewUserMap.delegate = self;
}
Before going any further, you will import the image that you will be using instead of a pin into your project. For this, I have chosen a small image called “avatar.png
”, shown here in Figure 5–14.
Obviously this image is too large to use on a map, so you will be scaling it down later.
The best way to import the file is to simply drag the file from the Finder into your navigation pane. I prefer to put such files under the Supporting Files group. In the dialog that appears, make sure the box marked “Copy items into destination group's folder (if needed)” is checked. Figure 5–15 should resemble the window in which this option appears, with the specific box at the top.
Next, you will create your annotation class. Select File New New File…, and under “Cocoa Touch” select “Objective-C class”. On the next screen, you will name your class “MyAnnotation
”, and make sure it is a subclass of NSObject
. Figure 5–16 shows this configuration.
After clicking Next and then Create, you will begin to edit this class.
The first thing you need to do is import your frameworks with the following lines:
#import <CoreLocation/CoreLocation.h>
#import <MapKit/MapKit.h>
Next, you will make sure that your class conforms to the MKAnnotation
protocol. To do this, you add <MKAnnotation>
to the header of your class, and you will declare three properties: coordinate
, title
, and subtitle
. You will also declare a designated initialization method in order to create your annotations with coordinates. Your header file will look like so after all these changes:
#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
#import <MapKit/MapKit.h>
@interface MyAnnotation : NSObject <MKAnnotation>
@property (nonatomic) CLLocationCoordinate2D coordinate;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subtitle;
-(id)initWithCoordinate:(CLLocationCoordinate2D)coord;
@end
Now you simply need to build your implementation file. Since you are not doing anything horribly complex with these annotations, simply their views, your MyAnnotation.m
file will look like so:
#import "MyAnnotation.h"
@implementation MyAnnotation
@synthesize coordinate, title, subtitle;
-(id)initWithCoordinate:(CLLocationCoordinate2D)coord
{
self = [super init];
if (self)
{
self.coordinate = coord;
}
return self;
}
@end
Now you can proceed to create your subclass of MKAnnotationView
. Start off by going to File New New File…, and under “Cocoa Touch” choose “Objective-C class”, just as before. On the next screen, you will name your class “CustomAnnotationView
”. The key here is to make sure you set the parent object correctly. Under “Subclass of”, replace “NSObject” by typing MKAnnotationView in its place, as shown in Figure 5–17.
Click Next and then Create to proceed.
In the header file of your CustomAnnotationView
, you will need to declare only one method to initialize your class, so that your header file looks like so:
#import <MapKit/MapKit.h>
@interface CustomAnnotationView : MKAnnotationView
-(id)initWithAnnotation:(id <MKAnnotation>) annotation reuseIdentifier:(NSString
*)annotationIdentifier;
@end
You may notice that this is the exact same name as the MKAnnotationView
's designated initializer. This is done on purpose, in order to make your subclassing as simple as possible. In your implementation file, you will start this method by simply starting the view off as if it were a regular MKAnnotationView
by calling the super's designated initializer:
-(id)initWithAnnotation:(id <MKAnnotation>) annotation reuseIdentifier:(NSString
*)annotationIdentifier
{
self = [super initWithAnnotation:annotation reuseIdentifier:annotationIdentifier];
return self;
}
Now, you can add to this initWithAnnotation:reuseIdentifier:
method to customize your view. First, you will create the UIImage
that you will set as your view's image to be used instead of the pin, and set it as your image.
UIImage *myImage = [UIImage imageNamed:@"avatar.png"];
self.image = myImage;
It is highly unlikely that the image that you have used is of an appropriate size to be used on a map. To offset this, you will standardize the size of these custom views by setting the frame of the view, and changing the content scaling mode to best scale your image.
self.frame = CGRectMake(0, 0, 40, 40);
self.contentMode = UIViewContentModeScaleAspectFill;
If necessary, you can also adjust the position of the image relative to the coordinates by using the centerOffset
property. This is especially useful if the image you are using has a particular point, such as a pin or arrow, that you would like to have at the exact coordinates. The centerOffset
property takes a CGPoint
value, with the easiest way to make one being through the CGPointMake()
function.
self.centerOffset = CGPointMake(1, 1);
Overall, your full method, along with a quick check to ensure that the annotation was initialized correctly, will resemble that shown here.
-(id)initWithAnnotation:(id <MKAnnotation>) annotation reuseIdentifier:(NSString
*)annotationIdentifier
{
self = [super initWithAnnotation:annotation reuseIdentifier:annotationIdentifier];
if (self)
{
//Create your UIImage to be used.
UIImage *myImage = [UIImage imageNamed:@"avatar.png"];
//Set your view's image
self.image = myImage;
//Standardize your AnnotationView's size.
self.frame = CGRectMake(0, 0, 40, 40);
//Use contentMode to ensure best scaling of image
self.contentMode = UIViewContentModeScaleAspectFill;
//Use centerOffset to adjust the image's position
self.centerOffset = CGPointMake(1, 1);
}
return self;
}
NOTE: All your customization points are placed in the if (self){}
block in order to ensure that your view has been correctly initialized. This is not entirely necessary, but merely a matter of good practice. In the case that your self
is not correctly initialized, the condition will evaluate as false, causing the method to simply return nil
.
Now that your custom classes are all set up, you can return to your view controller in order to implement your map's delegate method. This is done almost exactly the same as with a regular implementation, but you must change the type of MKAnnotationView
created from “MKPinAnnotationView
” to your “CustomAnnotationView
”. Remember that your application will not work if you have not imported the MyAnnotation.h
and CustomAnnotationView.h
files.
- (MKAnnotationView *)mapView:(MKMapView *)mapView
viewForAnnotation:(id<MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MyAnnotation class]])
{
static NSString *annotationIdentifier=@"annotationIdentifier";
MKAnnotationView *annotationView=[self.mapViewUserMap
dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];
annotationView.annotation = annotation;
if(!annotationView)
{
annotationView=[[CustomAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:annotationIdentifier];
}
annotationView.canShowCallout = YES;
return annotationView;
}
return nil;
}
Finally, all you need to run this is some test data. In your -viewDidLoad
method, you will add the following lines to create a few annotations and add them to your map. You will give them each a title and subtitle as well for testing purposes.
MyAnnotation *test1 = [[MyAnnotation alloc]
initWithCoordinate:CLLocationCoordinate2DMake(37.68, -97.33)];
test1.title = @"test1";
test1.subtitle = @"subtitle";
MyAnnotation *test2 = [[MyAnnotation alloc]
initWithCoordinate:CLLocationCoordinate2DMake(41.500, -81.695)];
test2.title = @"test2";
test2.subtitle = @"subtitle2";
[self.mapViewUserMap addAnnotation:test1];
[self.mapViewUserMap addAnnotation:test2];
NOTE: Even if an MKAnnotationView
's canShowCallout
property is set to YES, the callout will not display unless the view's annotation has been given a title. The subtitle is optional.
At this point, when you run the app, you should see your two annotations appear on the map with your image (shrunk down to a reasonable size) over Wichita, KS and Cleveland, OH. Figure 5–18 is the simulation of this app.
Now you will add a few extra lines of code to customize your callouts.
First, you will place an image to the left of the annotation's title and subtitle. This is done through the use of the annotationView
's property leftCalloutAccessoryView.
Your CustomAnnotationView class inherits a getter method for this from MKAnnotationView, so we will override this to make all our custom views display a specific image. You will do this by adding the following method to your CustomAnnotationView.m
file.
-(UIView *)leftCalloutAccessoryView
{
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage
imageNamed:@"avatar.png"]];
imageView.frame = CGRectMake(0, 0, 20, 20);
imageView.contentMode = UIViewContentModeScaleAspectFill;
return imageView;
}
This is very similar to the way you set your image for the annotation view, but with an extra step of having to create a UIImageView
to hold your avatar.png
image. You resize it down again (this time even smaller) and then return it. Since UIImageView
is a subclass of UIView
, this works perfectly well.
Just as you can edit the leftCalloutAccessoryView
, you can do the same thing on the right side of the callout. Here, you'll simply place a small disclosure button that can then be used to perform further functions.
-(UIView *)rightCalloutAccessoryView
{
return [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
}
With this addition, your annotation callouts will resemble those in Figure 5–19.
At this point, your callouts are all set up visually, but there's a massive amount of potential in having those buttons inside the callouts that you haven't tapped into yet. Most map-based apps that use buttons on their callouts will usually use the button to push another view controller onto the screen. An application focused on displaying the locations of a specific business on the map might allow the user to view all the details or pictures from a specific location.
In order to increase your functionality, you will implement another one of your map's delegate methods, -mapView:annotationView:calloutAccessoryControlTapped:
, and have it present a modal view controller. For demonstration purposes, you will have it display only your particular annotation's title and subtitle, but it is quite easy to see how this could be used much more extensively.
First, you will create your new view controller to be presented modally. Go to File New New File…. Under “Cocoa Touch”, select “UIViewController subclass”. Name the file “DetailViewController”, and make sure that the box marked Targeted for iPad is unchecked (unless you are developing on the iPad), and the box marked With XIB for User Interface is checked, as in Figure 5–20.
Up next, you will go into your DetailViewController
's .xib
file, and add in your labels. Drag two UILabel
s from the object library out into the view. Change their names to “title” and “subtitle”. Your user interface will look like Figure 5–21.
Now, you will connect these two labels over to your DetailViewController
's header file. Just as with your MKMapView
, hold ^ and drag the labels over into your header file. You will name their respective properties titleLabel
and subtitleLabel
.
Next, in the rest of your DetailViewController
, you will need a couple of NSString
variables to store your title and subtitle, so you will simply declare them as instance variables. You will also need to declare a designated initializer method that takes two NSString
s that you will set your labels to. Your header file will now look like so:
#import <UIKit/UIKit.h>
@interface DetailViewController : UIViewController {
UILabel *titleLabel;
UILabel *subtitleLabel;
NSString *myTitle;
NSString *mySubtitle;
}
@property (strong, nonatomic) IBOutlet UILabel *titleLabel;
@property (strong, nonatomic) IBOutlet UILabel *subtitleLabel;
-(id)initWithTitle:(NSString *)title subtitle:(NSString *)subtitle;
@end
The implementation of your designated initializer will look like so:
-(id)initWithTitle:(NSString *)title subtitle:(NSString *)subtitle
{
self = [super init];
if (self)
{
myTitle = title;
mySubtitle = subtitle;
}
return self;
}
Finally, you need to add the following two lines to your -viewDidLoad
method in order to set your labels' text once the view is loaded.
self.titleLabel.text = myTitle;
self.subtitleLabel.text = mySubtitle;
Finally, you are ready to implement your map's delegate method back in your main view controller. In this method, you will create your DetailViewController
and give it the necessary text, set it to do only a partial curl transition, and then present it. After you have correctly imported your header file using #import “DetailViewController.h”
, your method implementation will look like so:
-(void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view
calloutAccessoryControlTapped:(UIControl *)control
{
MyAnnotation *ann = view.annotation;
DetailViewController *dvc = [[DetailViewController alloc] initWithTitle:ann.title
subtitle:ann.subtitle];
dvc.modalTransitionStyle = UIModalTransitionStylePartialCurl;
[self presentViewController:dvc animated:YES completion:^{}];
}
NOTE: It is necessary to declare the variable ann
as an instance of MyAnnotation
in order to assure the compiler that the annotation you are being given will have the properties of .title
and .subtitle
that you are asking for.
Once this code has been added, your application should resemble Figure 5–22 when a detail disclosure button is pressed.
Annotations are not the only thing that can be added to a map. Here, you will go over how to add overlays to a map, which can take on a variety of shapes, from circles to polygons to lines. This recipe will build off of the first recipe in this chapter.
You will be adding two kinds of overlays to your MapView
, both polygon overlays and line overlays. The process to add these is very similar to that of adding annotations, but you do not have to create a separate class for the overlays like you did with the annotations.
First, you will implement the MapView
delegate method to tell your MapView
how to deal with overlays. You will be setting up your method to handle both polygons and lines by using the class
methods to check the type of overlay.
-(MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id )overlay{
if([overlay isKindOfClass:[MKPolygon class]]){
MKPolygonView *view = [[MKPolygonView alloc] initWithOverlay:overlay];
//Display settings
view.lineWidth=1;
view.strokeColor=[UIColor blueColor];
view.fillColor=[[UIColor blueColor] colorWithAlphaComponent:0.5];
return view;
}
else if ([overlay isKindOfClass:[MKPolyline class]])
{
MKPolylineView *view = [[MKPolylineView alloc] initWithOverlay:overlay];
//Display settings
view.lineWidth = 3;
view.strokeColor = [UIColor blueColor];
return view;
}
return nil;
}
Now you just need to create your overlays and add them to the MapView
by adding the following code to -viewDidLoad
.
//Create and Add Overlays
NSMutableArray *overlays = [[NSMutableArray alloc] initWithCapacity:2];
CLLocationCoordinate2D polyCoords[5]={
CLLocationCoordinate2DMake(39.9, -76.6),
CLLocationCoordinate2DMake(36.7, -84.0),
CLLocationCoordinate2DMake(33.1, -89.4),
CLLocationCoordinate2DMake(27.3, -80.8),
CLLocationCoordinate2DMake(39.9, -76.6)
};
MKPolygon *Poly = [MKPolygon polygonWithCoordinates:polyCoords count:5];
[overlays addObject:Poly];
CLLocationCoordinate2D pathCoords[2] = {
CLLocationCoordinate2DMake(46.8, -100.8),
CLLocationCoordinate2DMake(43.7, -70.4)
};
MKPolyline *pathLine = [MKPolyline polylineWithCoordinates:pathCoords count:2];
[overlays addObject:pathLine];
[self.mapViewUserMap addOverlays:overlays];
CAUTION: When making MKPolygon
s, your last coordinate point should be the same as your first one. If not, you will have issues getting the correct shapes.
When you run the app now, you should see the screen in Figure 5–23 when you zoom out on the map. You will also notice the polygon and line that we've set up are rather strange in shape and location, as they are simply meant to demonstrate what you can do with overlays on a MapView
.
Overlays are incredibly useful in Map Kit apps, and can also be very easily customized. Most of the properties such as color or line width can be changed, allowing you to fully customize how your app looks and acts. Overlays can also be created from circles, as well as other user-defined shapes. Refer to the Apple documentation for details on how to use any of these other functions.
A common issue when it comes to using annotations on a map view is the possibility of having very large numbers of annotations very close to each other, cluttering up the screen and making the application difficult to use. The solution is to group annotations together based on location and the size of the visible map. You will use a simple algorithm that compares location coordinates every time the visible region changes.
First, you need to create your project, naming it “HotspotMap”, with a class prefix of “CSD”, add the Map Kit and Core Location, and be sure to import them into your classes. Refer to Recipe 5–1 in this chapter for details on how to do this.
As is necessary when adding annotations to a map (see Recipe 5–2), you need to create a subclass of NSObject
that conforms to the MKAnnotation
protocol. Create the class by making a new file, choosing “Objective-C class”, and making sure it is a subclass of NSObject
. You will call your class “Hotspot
”, as shown in Figure 5–24.
You will define your class with a few different properties. Since it is conforming to the MKAnnotation
protocol, you will need a coordinate
, as well as two NSString
s, a title
, and a subtitle
. You will also need a designated initialization method in order to create your annotations with coordinates. You will define these in your header file, like so:
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <CoreLocation/CoreLocation.h>
@interface Hotspot : NSObject <MKAnnotation>
@property (nonatomic) CLLocationCoordinate2D coordinate;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subtitle;
-(id)initWithCoordinate:(CLLocationCoordinate2D) c;
@end
NOTE: Pay close attention to the import statements in this code, as well as the protocol directive <MKAnnotation>
in the interface header. Without these, your app will not run correctly.
Now you simply need to synthesize these properties and implement your initialization method in your .m
file, like so:
@synthesize coordinate, title, subtitle;
-(id)initWithCoordinate:(CLLocationCoordinate2D) c
{
self=[super init];
if(self){
coordinate = c;
}
return self;
}
Next, you will move over to your view controller. The first thing to do is put your MapView
in using Interface Builder, and link it over to your header file. In this example, I have named your MKMapView
“mapViewUserMap”. (Refer to Recipe 5–1 in this chapter on how to do this.) Also be sure to set your MapView
's delegate to your view controller in -viewDidLoad
, with the following line of code:
self.mapViewUserMap.delegate = self;
You will also need a few helper variables throughout your program. First, you need a variable of type CLLocationDegrees
, with which you will keep track of your current zoom level. Second, you will need a mutable array property, which you will use to hold all of your hotspots. Your header file will look something like this:
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import "Hotspot.h"
@interface CSDViewController : UIViewController <MKMapViewDelegate>
{
MKMapView *mapViewUserMap;
CLLocationDegrees zoom;
}
@property (strong, nonatomic) IBOutlet MKMapView *mapViewUserMap;
@property (strong, nonatomic) NSMutableArray *places;
@end
Next, in your implementation file, after synthesizing your places
property, you'll define a few constants that will set up your starting coordinates and grouping parameters. These will also be used to help generate some random locations for demonstration purposes. Place the following statements before your import statements in the .m
file.
#define centerLat 39.2953
#define centerLong -76.614
#define spanDeltaLat 4.9
#define spanDeltaLong 5.8
#define scaleLat 9.0
#define scaleLong 11.0
Before you get any further, you need to remember a very important aspect of dealing with NSArray
s as properties, in that they do not automatically allocate or initialize themselves in their synthesized accessor method. You will need to define your own accessor method in order to lazily instantiate the array. Since you will be making 1,000 testing locations soon, you will give the array an initial capacity of 1,000, like so:
-(NSMutableArray *)places
{
if (!places)
{
places = [[NSMutableArray alloc] initWithCapacity:1000];
}
return places;
}
Next, you will need some testing data. The following two methods will generate a good 1,000 hotspots for you to use, all within fairly close proximity to each other, so that you can see what kind of issue you are working with.
-(float)RandomFloatStart:(float)a end:(float)b
{
float random = ((float) rand()) / (float) RAND_MAX;
float diff = b - a;
float r = random * diff;
return a + r;
}
-(void)loadDummyPlaces
{
srand((unsigned)time(0));
for (int i=0; i<1000; i++)
{
Hotspot *place=[[Hotspot alloc]
initWithCoordinate:CLLocationCoordinate2DMake([self RandomFloatStart:37.0
end:42.0],[self RandomFloatStart:-72.0 end:-79.0])];
place.title = [NSString stringWithFormat:@"Place %d title",i];
place.subtitle = [NSString stringWithFormat:@"Place %d subtitle",i];
[self.places addObject:place];
}
}
Now that you have your testing data, you'll make sure your -viewDidLoad
method is all set up to do what you need. It should look like so:
- (void)viewDidLoad
{
[super viewDidLoad];
self.mapViewUserMap.delegate = self;
[self loadDummyPlaces];
[self.mapViewUserMap addAnnotations:self.places]; //For setup purposes only. This
line will be unnecessary when grouping is implemented.
CLLocationCoordinate2D centerPoint = {centerLat, centerLong};
MKCoordinateSpan coordinateSpan = MKCoordinateSpanMake(spanDeltaLat,
spanDeltaLong);
MKCoordinateRegion coordinateRegion = MKCoordinateRegionMake(centerPoint,
coordinateSpan);
[self.mapViewUserMap setRegion:coordinateRegion];
[self.mapViewUserMap regionThatFits:coordinateRegion];
}
Before you finish up your initial setup, you'll jump down to your -viewDidUnload
method and add in a few lines to keep your memory clean. The following two lines will help recycle memory if you ever want to use this class in a larger application later.
[self.places removeAllObjects];
self.places = nil;
Finally, you will need to implement your map's viewForAnnotation
method so that you can correctly display your pins. This is very similar to your previous recipe, as shown here:
- (MKAnnotationView *)mapView:(MKMapView *)mV viewForAnnotation:(id
<MKAnnotation>)annotation{
// if it's the user location, just return nil.
if ([annotation isKindOfClass:[MKUserLocation class]]){
return nil;
}
else{
static NSString *StartPinIdentifier = @"PinIdentifier";
MKPinAnnotationView *startPin = (id)[mV
dequeueReusableAnnotationViewWithIdentifier:StartPinIdentifier];
if (startPin == nil) {
startPin = [[MKPinAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:StartPinIdentifier];
}
startPin.canShowCallout = YES;
return startPin;
}
}
At this point, if you run the application, you should see a view resembling Figure 5–25, a perfect illustration of the problem you are trying to solve. Now that you have your test data set up, you will work on implementing the solution.
In order to properly iterate through your annotations and group them, you will be going through each hotspot and determining how it should be placed. If it is close to another pin that has already been dropped, it will be considered “found,” and will be removed from the map. If not, you will add it to the list of those already in the map, and add it to the map itself as an annotation. The following method provides an efficient implementation, and should be placed in your view controller's .m
file.
-(void)group:(NSArray *)hotspots{
float latDelta=self.mapViewUserMap.region.span.latitudeDelta/scaleLat;
float longDelta=self.mapViewUserMap.region.span.longitudeDelta/scaleLong;
NSMutableArray *visibleHotspots=[[NSMutableArray alloc] initWithCapacity:0];
for (Hotspot *current in hotspots) {
CLLocationDegrees lat = current.coordinate.latitude;
CLLocationDegrees longi = current.coordinate.longitude;
bool found=FALSE;
for (Hotspot *tempHotspot in visibleHotspots) {
if(fabs(tempHotspot.coordinate.latitude-lat) < latDelta &&
fabs(tempHotspot.coordinate.longitude-longi)<longDelta ){
[self.mapViewUserMap removeAnnotation:current];
found=TRUE;
break;
}
}
if (!found) {
[visibleHotspots addObject:current];
[self.mapViewUserMap addAnnotation:current];
}
}
}
NOTE: In this method, you use the fabs
function. This is different from the abs
function in that it is specifically used for floats. Using the abs
function here would result in grouping only at the integer level of coordinates, and your app would not work correctly.
Next, you need to deal with your application re-grouping the points every time the visible section of the map is changed. This is fairly easy to do by implementing the following delegate method:
-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated{
if (zoom!=mapView.region.span.longitudeDelta) {
[self group:places];
zoom=mapView.region.span.longitudeDelta;
}
}
NOTE: When implementing these methods, make sure that any methods that use the -group:
method are implemented after it, otherwise the compiler will complain. Another way to solve this problem is to simply define -(void)group(NSArray *)hotspots;
in your header file.
This method will check to see if the user has zoomed in or out, and if so, re-group the annotations accordingly. Now you just need to make one change to your -viewDidLoad
method. You can remove the following line, as its function will be performed by your group:
method.
[self.mapViewUserMap addAnnotations:self.places];
You actually should not need to call [self group:self.places];
at the end of your viewDidLoad
method, because when the map is first displayed, your delegate method -mapView: regionDidChangeAnimated:
will be called, and will do the initial grouping automatically.
Upon running the app now, you should see your map populated with significantly fewer annotations, with a somewhat regular distance in between them, as in Figure 5–26. Upon zooming in or out, you can see annotations appear or disappear respectively as the map changes.
Now that you have successfully grouped your annotations, you may choose to animate your pins as they are added to drop down, so that your ungrouping transition looks smoother. This can be done with a simple “startPin.animatesDrop = YES;
” in your viewForAnnotation:
method, directly after the “startPin.canShowCallout = YES;
” line.
While your annotations are correctly grouping at this point, you have a new issue, in that you cannot easily tell whether a single annotation is standing on its own, or if it is encapsulating multiple hotspots. In order to correct this, you will add in functionality to allow hotspots to keep track of the number of other hotspots they represent.
First, you will need to go to your Hotspot
class, and add in an NSMutableArray
property, “places
”. You will also add a few method definitions that you will use shortly to help manage this array. The following lines need to be added to “Hotspot.h
”.
@property (nonatomic, strong) NSMutableArray *places;
-(void)addPlace:(Hotspot *)hotspot;
-(int)placesCount;
Not only do you need to implement these methods, but you also need to change your -initWithCoordinate:
method in order to ensure that your places
array is correctly created. You will also have to implement your own version of your title
property's getter, so that the callout title will show the number of hotspots represented. Your implementation file now looks like so:
#import "Hotspot.h"
@implementation Hotspot
@synthesize coordinate, title, subtitle;
@synthesize places;
-(id)initWithCoordinate:(CLLocationCoordinate2D) c
{
self=[super init];
if(self){
coordinate = c;
self.places = [[NSMutableArray alloc] initWithCapacity:0];
}
return self;
}
-(NSString *)title
{
if ([self placesCount] == 1)
{
return title;
}
else
return [NSString stringWithFormat:@"%i Places", [self.places count]];
}
-(void)addPlace:(Hotspot *)hotspot
{
[self.places addObject:hotspot];
}
-(int)placesCount{
return [self.places count];
}
-(void)cleanPlaces{
[self.places removeAllObjects];
[self.places addObject:self];
}
@end
The foregoing -placesCount
method is not necessary; it just makes accessing the number of places represented by a single hotspot slightly easier. Your -cleanPlaces
method will be used simply to reset the places array whenever you re-group your annotations. All you have to do now is make sure your -group:
method correctly calls -cleanPlaces
by adding the following two lines in their appropriate places, which you will see in the full method implementation that follows.
[hotspots makeObjectsPerformSelector:@selector(cleanPlaces)];
[tempHotspot addPlace:current];
Your full -group:
method should now look like so:
-(void)group:(NSArray *)hotspots{
float latDelta=self.mapViewUserMap.region.span.latitudeDelta/scaleLat;
float longDelta=self.mapViewUserMap.region.span.longitudeDelta/scaleLong;
//New lines:
[hotspots makeObjectsPerformSelector:@selector(cleanPlaces)];
//End of new lines.
NSMutableArray *visibleHotspots=[[NSMutableArray alloc] initWithCapacity:0];
for (Hotspot *current in hotspots) {
CLLocationDegrees lat = current.coordinate.latitude;
CLLocationDegrees longi = current.coordinate.longitude;
bool found=FALSE;
for (Hotspot *tempHotspot in visibleHotspots) {
if(fabs(tempHotspot.coordinate.latitude-lat) < latDelta &&
fabs(tempHotspot.coordinate.longitude-longi)<longDelta ){
[self.mapViewUserMap removeAnnotation:current];
found=TRUE;
//New lines:
[tempHotspot addPlace:current];
//End of new lines.
break;
}
}
if (!found) {
[visibleHotspots addObject:current];
[self.mapViewUserMap addAnnotation:current];
}
}
}
Now you have a fairly easy way to determine whether any given hotspot is representing any other hotspots, but only by selecting that specific hotspot. It would be much better if you could easily see which hotspots are groups, and which are individuals. To do this, you will give each hotspot a pointer to its own MKPinAnnotationView
. From there, you can control which color they are based on the number of places they represent.
First, you will add the following property to your hotspot file. Don't forget to @synthesize it in the .m
file!
@property (nonatomic, strong) MKPinAnnotationView *annotationView;
Next you need to tell your map's delegate how to display the pins correctly, as shown in the new version of your -viewForAnnotation:
method here.
- (MKAnnotationView *)mapView:(MKMapView *)mV viewForAnnotation:(id
<MKAnnotation>)annotation{
// if it's the user location, just return nil.
if ([annotation isKindOfClass:[MKUserLocation class]]){
return nil;
}
else{
static NSString *StartPinIdentifier = @"PinIdentifier";
MKPinAnnotationView *startPin = (id)[mV
dequeueReusableAnnotationViewWithIdentifier:StartPinIdentifier];
if (startPin == nil) {
startPin = [[MKPinAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:StartPinIdentifier];
}
startPin.canShowCallout = YES;
startPin.animatesDrop = YES;
//NEW CODE
Hotspot *place = annotation;
place.annotationView = startPin;
if ([place placesCount] > 1)
{
startPin.pinColor = MKPinAnnotationColorGreen;
}
else if ([place placesCount] == 1)
{
startPin.pinColor = MKPinAnnotationColorRed;
}
//END OF NEW CODE
return startPin;
}
}
This will make all of your annotations correctly appear as either green or red, depending on whether they are groups or individualized. However, if you zoom in on a specific green annotation, it will not correctly change color as it goes from a group to an individual. As your final step, to correct this, you will add code to our -mapView:regionDidChangeAnimated:
to change the pin color based on the number of places represented, as shown here.
-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated{
if (zoom!=mapView.region.span.longitudeDelta) {
[self group:places];
zoom=mapView.region.span.longitudeDelta;
//NEW CODE
NSSet *visibleAnnotations = [mapView
annotationsInMapRect:mapView.visibleMapRect];
for (Hotspot *place in visibleAnnotations)
{
if ([place placesCount] > 1)
place.annotationView.pinColor = MKPinAnnotationColorGreen;
else
place.annotationView.pinColor = MKPinAnnotationColorRed;
}
//END OF NEW CODE
}
}
Now, any pins that represent groups of hotspots will be green, while individual ones will be red, as demonstrated in Figure 5–27.
The Map Kit framework is probably one of the most popularly used frameworks, purely for its powerful yet incredibly flexible ability to provide a fully customizable yet simplistic map interface. In this chapter, you have discussed the major capabilities of Map Kit, from locating the user, to adding annotations and overlays, to even the important issue of annotation grouping. However, you have only scratched the surface of the capabilities of Map Kit, especially in the areas of map-based problem solving. A quick look at the Map Kit documentation reveals the various other commands, methods, and properties you did not cover, which range from isolating particular sections of a map to entirely customizing how touch events are handled by the map. The effectiveness of these countless capabilities is limited only by the developer's imagination.