Chapter 5

Map Kit Recipes

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.

Recipe 5–1: Showing a Map with the Device's Location

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.

Image

Figure 5–1. Selecting a single view application

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.

Image

Figure 5–2. Configuring your project

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.

Image

Figure 5–3. Clicking the + button to add a framework

Image

Figure 5–4. Selecting the Core Location and Map Kit 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.

Image

Figure 5–5. .xib file with MKMapView and UILabel

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.

Image

Figure 5–6. Connecting the MKMapView to an outlet

Name the MKMapView outlet “mapViewUserMap”.

Image

Figure 5–7. Configuring the map view outlet

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.

Image

Figure 5–8. App's prompt to access location

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 Image Location Image 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.

Image

Figure 5–9. Adding a toolbar to the bottom of the .xib

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.

Image

Figure 5–10. Simulated application with panning and user tracking

NOTE: MKUserLocationFollowWithHeading is not functional in the iOS Simulator.

Recipe 5–2: Marking Locations with Pins

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 Image New Image New File, and under the Cocoa Touch category, choose “Objective-C class,” as in Figure 5–11.

Image

Figure 5–11. Creating an Objective-C class to act as an annotation

Name your class something clear like “MyAnnotation”, and make sure it is a subclass of NSObject, as shown in Figure 5–12.

Image

Figure 5–12. Configuring your NSObject subclass

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 Image, to simulate a pinch, and dragging.

Image

Figure 5–13. Application with map and pins

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.

Recipe 5–3: Creating Custom Annotations

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.

Image

Figure 5–14. Custom annotation image

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.

Image

Figure 5–15. Making sure the “Copy items” box is checked when adding files

Next, you will create your annotation class. Select File Image New Image 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.

Image

Figure 5–16. Subclassing NSObject to create annotations

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 Image New Image 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.

Image

Figure 5–17. Subclassing MKAnnotationView

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.

Image

Figure 5–18. Application with map and custom annotations

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.

Image

Figure 5–19. Map with custom annotations and callouts

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 Image New Image 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.

Image

Figure 5–20. Configuring a detail view controller

Up next, you will go into your DetailViewController's .xib file, and add in your labels. Drag two UILabels from the object library out into the view. Change their names to “title” and “subtitle”. Your user interface will look like Figure 5–21.

Image

Figure 5–21. DetailViewController .xib view

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

Image

Figure 5–22. Application responding to the tapping of callouts

Recipe 5–4: Adding Overlays to a Map

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 MKPolygons, 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.

Image

Figure 5–23. Map with geometric and path overlays

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.

Recipe 5–5: Grouping Annotations by Location

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.

Image

Figure 5–24. Configuring the Hotspot class

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 NSStrings, 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 NSArrays 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.

Image

Figure 5–25. A map with far too many annotations

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.

Image

Figure 5–26. Grouped annotations by location

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.

Image

Figure 5–27. Grouped annotations with number-specific colors

Summary

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.

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

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