Chapter    6

Map Recipes

The Map Kit framework is an incredibly powerful and useful toolkit that adds immense functionality to the location services that iOS 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.

With iOS 6, Apple made a fundamental change by replacing the Google Maps back-end with a map engine of its own. There are many improvements, including a new cartography that gives great looking maps at any zoom level, better zooming experience thanks to the seamless rendering, and of course the turn-by-turn navigation. Despite all the changes beneath, the Map Kit API has not changed, so code that worked on iOS 5 works without changes in iOS 6 as well.

Recipe 6-1: Showing a Map with the Current 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 an app with a map and allow the map to show the user’s location.

Setting Up the Application

Create a new single-view application and add the Map Kit framework and the Core Location framework to the project. You should also provide a location usage description in the application property list. This is done by adding the NSLocationUsageDescription key in the application’s Info.plist file. We’ve set its value to “Testing map views” and that description is used when the user is prompted and asked for permission to use location services. Figure 6-2 provides an example of this system alert box.

Note  Descriptions on how to link frameworks and setting values in the application’s property list file, can be found in Chapter 1.

When the frameworks have been added, you can start building the user interface. Select the view controller’s .xib file from the navigation pane, and drag a map view from the object library to the work space. Make it fill the entire view.

Next, add a label for displaying the current latitude and longitude of the device. Place it on top of the map, close to the bottom of the screen. Set the label’s text alignment to center justified. Also, to make the text easier to read on the map, set a background color (e.g., white). Your user interface should now look something like the one in Figure 6-1.

9781430245995_Fig06-01.jpg

Figure 6-1.  Main view controller with a map and a label

Create outlets for both the map view and the label. Name the outlets mapView and userLocationLabel, respectively. Because you haven’t imported the Map Kit API yet you’ll get an error indication next to the mapView property at this point. We’ll take care of that next.

Note  Chapter 1 provides detailed instructions on how to create outlets.

Your user interface is fully set up, so you can turn your attention to the view controller’s interface file (ViewController.h). You need to make two additions 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 make the view controller a map view delegate by adding MKMapViewDelegate as a supported protocol.

Your ViewController.h should now look something like this, with the foregoing changes in bold:

//
//  ViewController.h
//  Recipe 6.1: Showing Current Location
//

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

@interface ViewController : UIViewController<MKMapViewDelegate>

@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@property (weak, nonatomic) IBOutlet UILabel *userLocationLabel;

@end

Switch to the implementation file, ViewController.m, and the viewDidLoad method where you’ll initialize the map view. We’ll begin by explaining the steps and then show you the complete viewDidLoad method.

First, make the view controller be the map view’s delegate.

self.mapView.delegate = self;

Next, set the region of the map view. The region is the portion of the map that is currently being displayed. It 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. In this recipe, you start with a region of 10 by 10 kilometers over Baltimore, Maryland, in the United States.

// Set initial region
CLLocationCoordinate2D baltimoreLocation = CLLocationCoordinate2DMake(39.303, -76.612);
self.mapView.region =
    MKCoordinateRegionMakeWithDistance(baltimoreLocation, 10000, 10000);

Two optional properties worth mentioning are zoomEnabled and scrollEnabled. These control whether a user can zoom or pan the map, respectively.

// Optional Controls
//   self.mapView.zoomEnabled = NO;
//   self.mapView.scrollEnabled = NO;

Finally, you define the map as showing the user’s location. This is easily done by setting the showUserLocation property to YES. However, you should only set this property if location services are enabled on the device, like so:

//Control User Location on Map
if ([CLLocationManager locationServicesEnabled])
{
    self.mapView.showsUserLocation = YES;
}

Keep in mind that the feature to display the user’s location in the map requires an authorization from the user, which is prompted and asked for permission the first time the app is run.

Note  Just because showUserLocation is set to YES, the user’s location is not automatically visible on the map. To determine whether the location is visible in the current region, use the property userLocationVisible.

When you have specified that you want the map to display the user’s location, you can also make it track the user location by setting the userTrackingMode property or calling the setUserTrackingMode:animated: method.

The tracking mode can be one of three 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 is panned to keep the user’s location at the center. The top of the map is north. If the user pans the map manually, tracking stops.
  • MKUserTrackingModeFollowWithHeading: Map is panned to keep the user’s location at the center, and the map rotated so that the user’s heading is at the top of the map. If the user pans the map manually, tracking stops. This setting won’t work in the iOS simulator.

Initially, you are going to set userTrackingMode to MKUserTrackingModeFollow, but later we will show you how to give users the ability to control the tracking mode themselves:

//Control User Location on Map
if ([CLLocationManager locationServicesEnabled])
{
    self.mapView.showsUserLocation = YES;
    [self.mapView setUserTrackingMode:MKUserTrackingModeFollow animated:YES];
}

Your viewDidLoad method should now resemble the following:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.mapView.delegate = self;
    // Set initial region
    CLLocationCoordinate2D baltimoreLocation =
        CLLocationCoordinate2DMake(39.303, -76.612);
    self.mapView.region =
        MKCoordinateRegionMakeWithDistance(baltimoreLocation, 10000, 10000);
    // Optional Controls
    //    self.mapView.zoomEnabled = NO;
    //    self.mapView.scrollEnabled = NO;
    // Control User Location on Map
    if ([CLLocationManager locationServicesEnabled])
    {
        self.mapView.showsUserLocation = YES;
        [self.mapView setUserTrackingMode:MKUserTrackingModeFollow animated:YES];
    }

}

Finally, respond to location updates and update the label with the new location data. This is done in the mapView:didUpdateUserLocation: delegate method which you add to your view controller. Your implementation of the method looks like the following:

-(void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
{
    self.userLocationLabel.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. When the app launches on the simulator, the user is prompted to allow the app access to his or her location. Figure 6-2 shows your application displaying this prompt. Note that the message includes the location usage description if you provided it in the .plist file.

9781430245995_Fig06-02.jpg

Figure 6-2.  The app’s prompt to access location

If you tap OK and there is no sign of your location on the map, start one of the location debug services (e.g., Freeway Drive). On the simulator, go to the menu option Debug image Location image Freeway Drive, and this starts 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.

User-Controlled Tracking

If users try to pan the map manually, one of the problems they will encounter is that 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 toggles the user tracking modes on the specified map view.

To set this up, you add a toolbar to your user interface. You should adjust the size of the map view and move the label so that the toolbar doesn’t hide them. Then create an outlet named mapToolbar to reference the toolbar.

Delete the button that is added to the toolbar by default as you will not need it. You add a button programmatically in just a moment, but for now your user interface should resemble that in Figure 6-3.

9781430245995_Fig06-03.jpg

Figure 6-3.  Adding a toolbar to the bottom of the .xib

Now add your MKUserTrackingBarButtonItem in code. Switch to the view controller’s implementation file and scroll to the viewDidLoad method. Add the following code at the bottom of the method:

// Add button for controlling user location tracking
MKUserTrackingBarButtonItem *trackingButton =
    [[MKUserTrackingBarButtonItem alloc] initWithMapView:self.mapView];
[self.mapToolbar setItems: [NSArray arrayWithObject: trackingButton] animated:YES];

With this new add-on, users can manually pan the map and then get back to tracking their location with the tap of this new bar button. Figure 6-4 demonstrates the user-tracking functionality you have implemented.

9781430245995_Fig06-04.jpg

Figure 6-4.  Simulated application with panning and user tracking

Recipe 6-2: Marking Locations with Pins

A common usage of maps is to mark not only your current location and destination but also various points of interest along the route. In iOS, these highlights of points within the map are called annotations. Annotations, by default, look like pins, and Recipe 6-2 shows you how to add them to your map.

Using Recipe 6-2, you’ll build an application similar to the one in Recipe 6-1. Follow these steps to set up the application:

  1. Create a new single-view application project.
  2. Add the Map Kit framework to the project.
  3. Add the Core Location framework to the project. In this recipe you’re not going to use the location services so there’s no need to provide a location usage description. You still need to link in the framework though, or you’ll get linker errors when building later.
  4. Add a map view to the application’s main view.
  5. Create an outlet for referencing the map view. Name the outlet mapView.
  6. Make your view controller class conform to the MKMapViewDelegate protocol. The view controller’s header file should now look something like the following:
    //
    //  ViewController.h
    //  Recipe 6.2: Pinning Locations
    //

    #import <UIKit/UIKit.h>
    #import <MapKit/MapKit.h>

    @interface ViewController : UIViewController<MKMapViewDelegate>

    @property (weak, nonatomic) IBOutlet MKMapView *mapView;
    @end
  7. Finally, initialize the map view delegate property in the viewDidLoad method of the view controller.
- (void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    self.mapView.delegate = self;
}

Your application is now set up and you should build and run it to make sure everything is okay before moving on.

Adding Annotation Objects

Two objects are involved when you display an annotation on the map: the annotation object and the annotation view. The job of the annotation view is to draw an annotation. The annotation view is provided with a drawing context and an annotation object, which holds the data associated with the annotation. In its simplest form, an annotation object contains a title and a coordinate.

To create an annotation object you can use the built-in MKPointAnnotation class that holds properties for title, subtitle, and location. Then all you need to do is to add the annotations to the map view with its addAnnotation: or addAnnotations: method.

Let’s add a few annotations to the map view. Add the following code to the viewDidLoad method.

- (void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    self.mapView.delegate = self;

    MKPointAnnotation *annotation1 = [[MKPointAnnotation alloc] init];
    annotation1.title = @"Miami";
    annotation1.subtitle = @"Annotation 1";
    annotation1.coordinate = CLLocationCoordinate2DMake(25.802, -80.132);

    MKPointAnnotation *annotation2 = [[MKPointAnnotation alloc] init];
    annotation2.title = @"Denver";
    annotation2.subtitle = @"Annotation 2";
    annotation2.coordinate = CLLocationCoordinate2DMake(39.733, -105.018);

    [self.mapView addAnnotation:annotation1];
    [self.mapView addAnnotation:annotation2];

}

We chose to make the pins drop in Miami and Denver, but any coordinates work just as well. If you run this app now, you should see your map with two pins stuck in, as in Figure 6-5. You may need to zoom out to see them; this can be done in the simulator by holding Alt (⌥), to simulate a pinch, and dragging.

9781430245995_Fig06-05.jpg

Figure 6-5.  Application with map and pins

In the beginning of this section, we spoke of two objects being necessary to display an annotation. You may be wondering what happened to the second object, the annotation view. We didn’t create one, still the annotation objects show up on the map. The reason is that if you don’t provide it, the framework will create instances of the MKPinAnnotationView class and use them as annotation views.

Changing the Pin Color

The default annotation view displays your annotation as a red pin on the map. Usually, the red color indicates a destination location. The other two possible pin colors of an MKPinAnnotationView are green (for starting points) and purple (for user-defined points). If you want a color other than red for your pin, you’ll need to create the views yourself, which can be done in the mapView:viewForAnnotation: delegate method.

Let’s change the pin color to purple for the annotations. Start by adding the mapView:viewForAnnotation: method to your view controller.

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{

    // Returning nil will result in a default annotation view being used
    return nil;
}

The map view sends all annotations that are within its current range to the mapView:viewForAnnotation: method to retrieve the annotation view it uses to do the drawing. The user location, which is a special kind of annotation, also is sent to this method, so you need to make sure the provided annotation is of the type you expect.

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    // Don't create annotation views for the user location annotation
    if ([annotation isKindOfClass:[MKPointAnnotation class]])
    {

        // Create and return our own annotation view here
    }
    // Returning nil will result in a default annotation view being used
    return nil;
}

To minimize the number of annotation views needed, map views provide a way to reuse annotation views by caching them. The code to cache the views looks a lot like the one used to create cells for table views.

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    // Don't create annotation views for the user location annotation
    if ([annotation isKindOfClass:[MKPointAnnotation class]])
    {
        static NSString *userPinAnnotationId = @"userPinAnnotation";
        // Create an annotation view, but reuse a cached one if available
        MKPinAnnotationView *annotationView =
           (MKPinAnnotationView *)[self.mapView
           dequeueReusableAnnotationViewWithIdentifier:userPinAnnotationId];
        if(annotationView)
        {
            // Cached view found. It'll have the pin color set but not annotation.
            annotationView.annotation = annotation;
        }
        else
        {
            // No cached view were available, create a new one
            annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation
                              reuseIdentifier:userPinAnnotationId];

            // Purple indicates user defined pin
            annotationView.pinColor = MKPinAnnotationColorPurple;
        }
        return annotationView;

    }
    return nil;
}

Note  The identifier string should be different for every type of annotation view you create. For example, an annotation view that draws red pins should have a different ID than one that draws purple pins; otherwise you may get unexpected behavior when you retrieve views from the cache.

If you build and run now you should see the same pins as those in Figure 6-5, but they’ll be purple instead of red. Besides changing pin color, a lot more can be done to customize annotations. This is the topic of Recipe 6-3.

Recipe 6-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. Likewise, you may want to display more usable and pleasing looking callouts when the user taps your annotations. To create a custom annotation view, you will be subclassing the MKAnnotationView class. Using Recipe 6-3, you also create a custom annotation object to hold additional data, as well as a detailed view, which is displayed when the user taps your callouts.

Setting Up the Application

To set up the application, you must first create your project the same way you did in the previous recipes. Follow these steps to get your app skeleton set up:

  1. Create a new single-view application project.
  2. Add the Map Kit framework to the project.
  3. Add the Core Location framework to the project. In Recipe 6-3 you’re not going to use the location services so there’s no need to provide a location usage description. You still need to link in the framework though or you’ll get linker errors when building later.
  4. Add a map view to the application’s main view.
  5. Create an outlet for referencing the map view. Name the outlet mapView.
  6. Import the Map Kit API in ViewController.h.
  7. Make your view controller class conform to the MKMapViewDelegate protocol. The view controller’s header file should now look something like the following:
    //
    //  ViewController.h
    //  Recipe 6.3: Customizing Annotations
    //

    #import <UIKit/UIKit.h>
    #import <MapKit/MapKit.h>

    @interface ViewController : UIViewController<MKMapViewDelegate>

    @property (weak, nonatomic) IBOutlet MKMapView *mapView;
    @end
  8. Finally, initialize the map view delegate property in the viewDidLoad method of the view controller.
    - (void)viewDidLoad
    {
        [super viewDidLoad];
            // Do any additional setup after loading the view, typically from a nib.
        self.mapView.delegate = self;
    }

    Before going any further, you must add an image to be used instead of a pin. For Recipe 6-3 we have chosen a small image, overlay.png, shown here in Figure 6-6. You may, of course, pick any image you like.

9781430245995_Fig06-06.jpg

Figure 6-6.  The custom annotation image added to the project

The image is a bit large to use on a map, so you will be scaling it down later. However, in a real scenario you would scale down the image to the size of your annotation before adding it to the project. This way you save some space and clock cycles because the application won’t have to do the scaling at runtime.

Creating a Custom Annotation Class

The next step is to create your custom annotation class. Use the built-in Objective-C class template to create a new subclass of MKPointAnnotation. Name the new class MyAnnotation.

The MKPointAnnotation already contains properties for the title and the subtitle so you don’t have to declare them in your class. However, to show you how to attach custom data to your annotation objects, you’ll extend the class with an additional property to keep contact information. You’ll also add a designated initialization method to contain all the annotation setup code.

Go ahead and make the following changes to MyAnnotation.h.

//
//  MyAnnotation.h
//  Recipe 6.3: Customizing Annotations
//

#import <MapKit/MapKit.h>

@interface MyAnnotation : MKPointAnnotation

@property (nonatomic, strong) NSString *contactInformation;

-(id)initWithCoordinate:(CLLocationCoordinate2D)coord title:(NSString *)title subtitle:(NSString *)subtitle contactInformation:(NSString *)contactInfo;

@end

The following are corresponding changes to MyAnnotation.m:

//
//  MyAnnotation.m
//  Recipe 6.3: Customizing Annotations
//

#import "MyAnnotation.h"

@implementation MyAnnotation

-(id)initWithCoordinate:(CLLocationCoordinate2D)coord title:(NSString *)title subtitle:(NSString *)subtitle contactInformation:(NSString *)contactInfo
{
    self = [super init];
    if (self)
    {
        self.coordinate = coord;
        self.title = title;
        self.subtitle = subtitle;
        self.contactInformation = contactInfo;
    }
    return self;
}


@end

Note  As you may have realized we didn’t include a @synthesize declaration for the contactInformation property. Starting in Xcode 4.5 this is no longer necessary because the compiler synthesizes any nondeclared getters and setters automatically.

Creating a Custom Annotation View

Now you can proceed to create your custom annotation view. As before, create a new Objective-C class, this time with the name MyAnnotationView and MKAnnotationView as the parent class.

The only thing you do in the custom annotation view class is to override the initWithAnnotation:resuseIdentifier: method. There’s where all the customization takes place. But before we go ahead and do that, let’s take a quick look at the code that’s been generated for you in MyAnnotationView.m.

// ...
@implementation MyViewAnnotation
- (id)initWithFrame:(CGRect)frame
{
    // ...
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
*/
@end

Xcode has added an initWithFrame: method to your class. You will not need it so feel free to remove it. For your convenience, Xcode has also added the drawRect: method but commented it out. drawRect: is interesting because it provides a way to completely control the drawing of your annotation. We will not use it in this recipe so you can remove it as well.

Instead, add the following:

//
//  MyAnnotationView.m
//  Recipe 6.3: Customizing Annotations
//

#import "MyAnnotationView.h"

@implementation MyAnnotationView

- (id)initWithAnnotation:(id <MKAnnotation>)annotation
reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
    if (self)
    {
        UIImage *myImage = [UIImage imageNamed:@"overlay.png"];
        self.image = myImage;
        self.frame = CGRectMake(0, 0, 40, 40);
        // Use contentMode to ensure best scaling of image
        self.contentMode = UIViewContentModeScaleAspectFill;
        // Use centerOffset to adjust the position of the image
        self.centerOffset = CGPointMake(1, 1);
    }
    return self;
}


@end

The preceding code creates the custom annotation image to be used instead of the pin. Also, the frame of the annotation view is adjusted to 40 by 40 pixels that the image is scaled down to.

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.

Now that your custom classes are all set up, you can return to your view controller to implement your map’s delegate method. You’ll probably recognize a lot of it from the previous recipes. The main difference is that you don’t create instances from MKPinAnnotationView, but from the custom MyAnnotationView class.

//
//  ViewController.m
//  Recipe 6.3: Customizing Annotations
//

#import "ViewController.h"
#import "MyAnnotation.h"
#import "MyAnnotationView.h"


// ...

@implementation ViewController

// ...

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    // Don't create annotation views for the user location annotation
    if ([annotation isKindOfClass:[MyAnnotation class]])
    {
        static NSString *myAnnotationId = @"myAnnotation";
        // Create an annotation view, but reuse a cached one if available
        MyAnnotationView *annotationView =
            (MyAnnotationView *)[self.mapView
            dequeueReusableAnnotationViewWithIdentifier:myAnnotationId];
        if(annotationView)
        {
            // Cached view found, associate it with the annotation
            annotationView.annotation = annotation;
        }
        else
        {
            // No cached view were available, create a new one
            annotationView = [[MyAnnotationView alloc] initWithAnnotation:annotation
                              reuseIdentifier:myAnnotationId];
        }
        return annotationView;
    }
    // Use a default annotation view for the user location annotation
    return nil;
}


@end

Finally, all you need to run this is some test data. In the viewDidLoad method, add the following lines to create a couple of annotations and add them to your map:

@implementation ViewController
// ...

- (void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    self.mapView.delegate = self;
    MyAnnotation *ann1 = [[MyAnnotation alloc]
        initWithCoordinate: CLLocationCoordinate2DMake(37.68, -97.33)
        title: @"Company 1"
        subtitle: @"Something Catchy"
        contactInformation: @"Call 555-123456"];

    MyAnnotation *ann2 = [[MyAnnotation alloc]
        initWithCoordinate:CLLocationCoordinate2DMake(41.500, -81.695)
        title:@"Company 2"
        subtitle:@"Even More Catchy"
        contactInformation:@"Call 555-654321"];

    NSArray *annotations = [NSArray arrayWithObjects: ann1, ann2, nil];
    [self.mapView addAnnotations:annotations];

}

// ...

@end

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, Kansas and Cleveland, Ohio. Figure 6-7 provides a simulation of this app.

9781430245995_Fig06-07.jpg

Figure 6-7.  Application with map and custom annotations

Customizing the Callouts

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. You will also add an accessory button to the right-hand side of the callout. You’ll use it later to display a detailed view of the annotation.

Go back to MyAnnotationView.m and extend the initWithAnnotation:reuseidentifier: method with the following code:

- (id)initWithAnnotation:(id <MKAnnotation>)annotation
reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
    if (self)
    {
        UIImage *myImage = [UIImage imageNamed:@"overlay.png"];
        self.image = myImage;
        self.frame = CGRectMake(0, 0, 40, 40);
        //Use contentMode to ensure best scaling of image
        self.contentMode = UIViewContentModeScaleAspectFill;
        //Use centerOffset to adjust the position of the image
        self.centerOffset = CGPointMake(1, 1);

        self.canShowCallout = YES;

        // Left callout accessory view
        UIImageView *leftAccessoryView = [[UIImageView alloc] initWithImage:myImage];
        leftAccessoryView.frame = CGRectMake(0, 0, 20, 20);
        leftAccessoryView.contentMode = UIViewContentModeScaleAspectFill;
        self.leftCalloutAccessoryView = leftAccessoryView;

        // Right callout accessory view
        self.rightCalloutAccessoryView =
            [UIButton buttonWithType:UIButtonTypeDetailDisclosure];

    }
    return self;
}

As you can see, we’re reusing the annotation image but wrapping it into a image view to scale it down, this time to 20 by 20 pixels.

If you build and run your application now, your annotations will present callouts like the one in Figure 6-8.

9781430245995_Fig06-08.jpg

Figure 6-8.  Map with custom annotations, one of which is showing a callout

Adding a Detailed View

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

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 the purpose of this recipe, you will have it display only your particular annotation’s title, subtitle, and contact information texts.

Start by creating the new view controller. Name it DetailedViewController and make sure it’s a subclass of UIViewController, and that you have the “With XIB for user interface” option checked.

Up next, go into your DetailedViewController’s .xib file, and add three labels. Place them near the bottom of the view, as in Figure 6-9.

9781430245995_Fig06-09.jpg

Figure 6-9.  DetailedViewController.xib view

Now, create outlets for the three labels. Name them titleLabel, subtitleLabel, and contactInformationLabel, respectively. Also, make the following additions to the header file:

//
//  DetailedViewController.h
//  Recipe 6.3: Customizing Annotations
//

#import <UIKit/UIKit.h>
#import "MyAnnotation.h"

@interface DetailedViewController : UIViewController

@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UILabel *subtitleLabel;
@property (weak, nonatomic) IBOutlet UILabel *contactInfoLabel;

@property (strong, nonatomic) MyAnnotation *annotation;

-(id)initWithAnnotation:(MyAnnotation *)annotation;

@end

In DetailedViewController.m, start by removing the initWithNibName:bundle: method that Xcode added to your class. In its place, implement the initWithAnnotation: method you declared earlier.

//
//  DetailedViewController.m
//  Recipe 6.3: Customizing Annotations
//

// ...

@implementation DetailedViewController

// ...

-(id)initWithAnnotation:(MyAnnotation *)annotation
{
    self = [super init];
    if (self)
    {
        self.annotation = annotation;
    }
    return self;
}


// ...
@end

Now, in the viewDidLoad method of the detailed view controller, add code to initialize the labels with texts from the stored annotation object.

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    self.titleLabel.text = self.annotation.title;
    self.subtitleLabel.text = self.annotation.subtitle;
    self.contactInfoLabel.text = self.annotation.contactInformation;

}

Finally, you are ready to implement your map’s delegate method back in your main view controller. In this method, you create and present your DetailedViewController. For Recipe 6-3 chose the partial curl transition, which is a pretty cool effect. Here’s how you set it up.

-(void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
    DetailedViewController *dvc = [[DetailedViewController alloc]
        initWithAnnotation:view.annotation];
    dvc.modalTransitionStyle = UIModalTransitionStylePartialCurl;
    [self presentViewController:dvc animated:YES completion:^{}];
}

Now you’re done with Recipe 6-3. Your application should resemble Figure 6-10 when you tap a detail disclosure button in one of your customized callouts.

9781430245995_Fig06-10.jpg

Figure 6-10.  Application responding to the tapping of callouts

Recipe 6-4: Dragging a Pin

Using this recipe you’ll make a little tool that allows you to drag a pin on a map and read its location from the console.

Start by creating a new map-based application, just as you did in the previous recipes. Here are the steps again:

  1. Create a new single-view application project.
  2. Add the Map Kit framework to the project.
  3. Add the Core Location framework to the project.
  4. Add a map view to the application’s main view.
  5. Create an outlet for referencing the map view. Name the outlet mapView.
  6. Make your view controller class conform to the MKMapViewDelegate protocol. The view controller’s header file should now look something like the following:
    //
    //  ViewController.h
    //  Recipe 6.3: Customizing Annotations
    //

    #import <UIKit/UIKit.h>
    #import <MapKit/MapKit.h>

    @interface ViewController : UIViewController<MKMapViewDelegate>

    @property (weak, nonatomic) IBOutlet MKMapView *mapView;

    @end
  7. Finally, initialize the map view delegate property in the viewDidLoad method of the view controller.
    - (void)viewDidLoad
    {
        [super viewDidLoad];
            // Do any additional setup after loading the view, typically from a nib.
        self.mapView.delegate = self;
    }

Adding a Draggable Pin

You’re going to make a really simple tool with a single pin on the map that the user can drag around. Let’s start by placing the pin on the map when the application has loaded the main view.

- (void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    self.mapView.delegate = self;

    MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
    annotation.coordinate = CLLocationCoordinate2DMake(39.303, -76.612);
    [self.mapView addAnnotation:annotation];

}

We’ve put the pin in Baltimore, but soon you’ll be able to replace it with coordinates of your own using this tool. But first you need to make the pin draggable. To do that you need to customize the annotation view that displays your pin. The code is nearly identical to the one used in Recipes 6.2 and 6.3, except that you now set the draggable property.

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    // Don't create annotation views for the user location annotation
    if ([annotation isKindOfClass:[MKPointAnnotation class]])
    {
        static NSString *draggableAnnotationId = @"draggableAnnotation";

        // Create an annotation view, but reuse a cached one if available
        MKPinAnnotationView *annotationView =
        (MKPinAnnotationView *)[self.mapView
            dequeueReusableAnnotationViewWithIdentifier:draggableAnnotationId];
        if(annotationView)
        {
            // Cached view found, associate it with the annotation
            annotationView.annotation = annotation;
        }
        else
        {
            // No cached view were available, create a new one
            annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation
                reuseIdentifier:draggableAnnotationId];
            annotationView.pinColor = MKPinAnnotationColorPurple;
            annotationView.draggable = YES;
        }
        return annotationView;
    }
    // Use a default annotation view for the user location annotation
    return nil;
}

If you run the application now you can drag the pin from Baltimore to any other place of your choice. However, let’s turn this albeit cool but somewhat useless app into a tool. You are going to intercept when the user drops the pin and output the new location to the console. To do this you will make use of another delegate method, the long but explanatory named mapView:annotationView:didChangeDragState:fromOldState:.

-(void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view didChangeDragState:(MKAnnotationViewDragState)newState fromOldState:(MKAnnotationViewDragState)oldState
{
    if (newState == MKAnnotationViewDragStateEnding)
    {
        MKPointAnnotation *annotation = view.annotation;
        NSLog(@" Pin Location: %f, %f (Lat, Long)",
              annotation.coordinate.latitude, view.annotation.coordinate.longitude);
    }
}

Run the application in the simulator and drag the pin to a new location. The console now shows the world coordinate of the pin, as in Figure 6-11. The tool could be quite useful if you want to create some location test data of your own.

9781430245995_Fig06-11.jpg

Figure 6-11.  The tool prints new pin locations to the console

Not bad for a handful of lines of code.

Recipe 6-5: Adding Overlays to a Map

An annotation, as you’ve seen in the previous recipes, is a marking on a map. Because annotations are associated with a single coordinate they stay the same size at all times, even when the user zooms in or out in the map.

This recipe looks at another type of map marking, so-called overlays. These are shapes such as circles or polygons, and unlike annotations, they scale when the map zoom changes.

You will be adding three kinds of overlays to your MapView: circle, polygon, and line overlays. The process to add these is very similar to that of adding annotations, but this time you will not create a custom class for the overlays like you did with annotations in Recipe 6-3.

Again, start by setting up a new map-based application. We trust you know the steps by now, but you could always look back at the previous recipes for guidance.

Creating the Overlays

Again you will create your test data in the viewDidLoad method of your view controller. First out is a circle overlay over big parts of Mexico.

CLLocationCoordinate2D mexicoCityLocation = CLLocationCoordinate2DMake(19.808, -98.965);
MKCircle *circleOverlay = [MKCircle circleWithCenterCoordinate:mexicoCityLocation
                           radius:500000];

Then, a polygon overlay. Note that a polygon must always start and end in the same location.

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 *polygonOverlay = [MKPolygon polygonWithCoordinates:polyCoords count:5];

And a line overlay.

CLLocationCoordinate2D pathCoords[2] =
{
    CLLocationCoordinate2DMake(46.8, -100.8),
    CLLocationCoordinate2DMake(43.7, -70.4)
};
MKPolyline *pathOverlay = [MKPolyline polylineWithCoordinates:pathCoords count:2];

Finally, add the three overlays to the map.

[self.mapView addOverlays:
    [NSArray arrayWithObjects: circleOverlay, polygonOverlay, pathOverlay, nil]
];

If you build and run your application at this point, you’ll see a map but none of the overlays you’ve created and added. This is because you haven’t provided the overlay view objects. This is done in the mapView:viewForOverlay: delegate method, which you’ll add to your view controller:

-(MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id )overlay
{
    if([overlay isKindOfClass:[MKCircle class]])
    {
        MKCircleView *view = [[MKCircleView alloc] initWithOverlay:overlay];
        //Display settings
        view.lineWidth = 1;
        view.strokeColor = [UIColor blueColor];
        view.fillColor = [[UIColor blueColor] colorWithAlphaComponent:0.5];
        return view;
    }
    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;
}

As you can see from the preceding code, each overlay shape type has a corresponding overlay view type that you use to instantiate the view objects. Each view class has similar properties for customizing the appearance of the overlay, such as colors and transparency (alpha component).

You’re finished with this recipe. When you build and run it you should see a screen resembling the one in Figure 6-12.

9781430245995_Fig06-12.jpg

Figure 6-12.  An app with a circle, a polygon, and a line overlay

Recipe 6-6: Grouping Annotations Dynamically

When it comes to using annotations on a map view, a common problem is the possibility of having many annotations appear very close to each other, cluttering up the screen and making the application difficult to use. One solution is to group annotations based on the location and the size of the visible map. Using Recipe 6-6, you employ a simple algorithm that, when the visible region changes, compares the location of the annotations and temporarily removes those that are too close to others.

First, you need to create a new map-based application project. Refer to Recipe 6-1 for details on how to do this.

A Forest of Pins

Let’s start by creating the test data, 1,000 pins randomly distributed within a relatively small area on the map. First you need a couple of instance variables, one to keep track of your current zoom level and a mutable array to hold your annotations. Make the following changes to your ViewController.h file:

//
//  ViewController.h
//  Recipe 6.6: Grouping Annotations Dynamically
//

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

@interface ViewController : UIViewController<MKMapViewDelegate>
{
    CLLocationDegrees _zoomLevel;
    NSMutableArray *_annotations;
}


@property (weak, nonatomic) IBOutlet MKMapView *mapView;

@end

Now switch to the implementation file and add the following code to instantiate the _annotations array. Give the array an initial capacity of 1,000 objects because you’ll be making that many annotations soon.

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.mapView.delegate = self;

    _annotations = [[NSMutableArray alloc] initWithCapacity:1000];
}

Next  create a custom annotation class. As usual, use the Objective-C class template to add the files to your project. Make sure to use NSObject as the parent class, and name the new class Hotspot.

To make the new class an annotation class, make it conform to the MKAnnotation protocol. Also, to make it easy to instantiate annotations with your new class, add an initializing method that takes a coordinate, a title, and a subtitle. The header file should now look like the following:

//
//  Hotspot.h
//  Recipe 6.6: Grouping Annotations Dynamically
//

#import <MapKit/MapKit.h>

@interface Hotspot : NSObject<MKAnnotation>
{
    CLLocationCoordinate2D _coordinate;
    NSString *_title;
    NSString *_subtitle;
}

@property (nonatomic) CLLocationCoordinate2D coordinate;
@property (nonatomic, readonly, copy) NSString *title;
@property (nonatomic, readonly, copy) NSString *subtitle;

-(id)initWithCoordinate:(CLLocationCoordinate2D)coordinate title:(NSString *)title subtitle:(NSString *)subtitle;


@end

And the corresponding implementation:

//
//  Hotspot.m
//  Recipe 6.6: Grouping Annotations Dynamically
//

#import "Hotspot.h"

@implementation Hotspot

-(id)initWithCoordinate:(CLLocationCoordinate2D)coordinate
title:(NSString *)title subtitle:(NSString *)subtitle
{
    self = [super init];
    if (self) {
        self.coordinate = coordinate;
        self.title = title;
        self.subtitle = subtitle;
    }
    return self;
}

-(CLLocationCoordinate2D)coordinate
{
    return _coordinate;
}

-(void)setCoordinate:(CLLocationCoordinate2D)coordinate
{
    _coordinate = coordinate;
}

-(NSString *)title
{
    return _title;
}

-(void)setTitle:(NSString *)title
{
    _title = title;
}

-(NSString *)subtitle
{
    return _subtitle;
}

-(void)setSubtitle:(NSString *)subtitle
{
    _subtitle = subtitle;
}


@end

You’ll also define a few constants that set up your starting coordinates and grouping parameters. These are also used to help generate some random locations for demonstration purposes. Place the following statements before your import statements in the Hotspot.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

Next, you need some testing data. Begin by importing Hotspot.h in at the top of your ViewController.m file.

#import "Hotspot.h"

The following two methods generate some 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)randomFloatFrom:(float)a to:(float)b
{
    float random = ((float) rand()) / (float) RAND_MAX;
    float diff = b - a;
    float r = random * diff;
    return a + r;
}

-(void)generateAnnotations
{
    srand((unsigned)time(0));

    for (int i=0; i<1000; i++)
    {
        CLLocationCoordinate2D randomLocation =
            CLLocationCoordinate2DMake(
                [self randomFloatFrom:37.0 to:42.0],
                [self randomFloatFrom:-72.0 to:-79.0]
            );
        Hotspot *place = [
            [Hotspot alloc]
            initWithCoordinate:randomLocation
            title: [NSString stringWithFormat:@"Place %d title", i]
            subtitle: [NSString stringWithFormat:@"Place %d subtitle", i]
        ];
        [_annotations addObject:place];
    }
}

Now that you have your method for generating testing data, you’ll make sure to invoke it in your viewDidLoad method, then add the annotations to the map and adjust its region to display them all. Here’s the code to do that.

- (void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    self.mapView.delegate = self;
    _annotations = [[NSMutableArray alloc] initWithCapacity:1000];

    [self generateAnnotations];
    // The line below is for setup purposes only. It will be unnecessary
    // when grouping is implemented.
    [self.mapView addAnnotations:_annotations];

    CLLocationCoordinate2D centerPoint = {centerLat, centerLong};
    MKCoordinateSpan coordinateSpan = MKCoordinateSpanMake(spanDeltaLat, spanDeltaLong);
    MKCoordinateRegion coordinateRegion =
        MKCoordinateRegionMake(centerPoint, coordinateSpan);

    [self.mapView setRegion:coordinateRegion];
    [self.mapView regionThatFits:coordinateRegion];

}

Finally, you need to implement your map’s viewForAnnotation method so that you can correctly display your pins. This code is similar to the one used in the previous recipes, as shown here:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
    // if it's the user location, just return nil.
    if ([annotation isKindOfClass:[MKUserLocation class]])
        return nil;
        else
    {
        static NSString *startPinId = @"StartPinIdentifier";
        MKPinAnnotationView *startPin =
            (id)[mapView dequeueReusableAnnotationViewWithIdentifier:startPinId];
                if (startPin == nil)
        {
            startPin = [[MKPinAnnotationView alloc]
                        initWithAnnotation:annotation
                        reuseIdentifier:startPinId];
            startPin.canShowCallout = YES;
            startPin.animatesDrop = YES;
        }
        return startPin;
    }
}

Note  When adding the mapView:viewForAnnotation: delegate method, you may get a compiler warning saying Local declaration of 'mapView' hides instance variable. This is because the parameter of the method and the mapView property share the same name. While the name clash in this case is not a real problem, you should always try to resolve warnings.

There are two ways to make the warning go away. The easiest is to rename the parameter, but the better way is to rename the property’s instance variable. This can be done by explicitly naming it in the @synthesize declaration.

@synthesize mapView = _mapView;

The property is still named mapView, but the underlying instance variable has the conventional underscore prefix and thus no longer is conflicting with delegate parameters.

At this point, if you run the application you should see a view resembling Figure 6-13, a nice illustration of the problem you are trying to solve.

9781430245995_Fig06-13.jpg

Figure 6-13.  A map with far too many annotations

Implementing a Solution

To properly iterate through your annotations and group them, you will be going through each pin 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 it 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 *)annotations
{
    float latDelta = self.mapView.region.span.latitudeDelta / scaleLat;
    float longDelta = self.mapView.region.span.longitudeDelta / scaleLong;
    NSMutableArray *visibleAnnotations = [[NSMutableArray alloc] initWithCapacity:0];
    for (Hotspot *current in annotations)
    {
        CLLocationDegrees lat = current.coordinate.latitude;
        CLLocationDegrees longi = current.coordinate.longitude;
        bool found = FALSE;
        for (Hotspot *temp in visibleAnnotations)
        {
            if(fabs(temp.coordinate.latitude - lat) < latDelta &&
               fabs(temp.coordinate.longitude - longi) < longDelta)
            {
                [self.mapView removeAnnotation:current];
                found = TRUE;
                break;
            }
        }
        if (!found)
        {
            [visibleAnnotations addObject:current];
            [self.mapView 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’s regrouping 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 (_zoomLevel != mapView.region.span.longitudeDelta)
    {
        [self group:_annotations];
        _zoomLevel = 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 declare the (void)group(NSArray *)annotations method in your header file or in a private @interface section.

Now you can remove the following line from the viewDidLoad method, as its function will be performed by your group: method.

[self.mapView addAnnotations:_annotations];

You don’t need to call the group: method at the end of your viewDidLoad method, because when the map is first displayed, your delegate method -mapView: regionDidChangeAnimated: is called and does the initial grouping automatically.

Upon running the app now, you should see your map populated with significantly fewer annotations, somewhat regularly distributed as in Figure 6-14. When zooming in or out, you can see annotations appear or disappear, respectively, as the map changes.

9781430245995_Fig06-14.jpg

Figure 6-14.  Grouped annotations by location

Adding Color Coding

While your annotations are correctly grouping at this point, you have a new issue. You cannot easily tell whether a single annotation is standing on its own or whether it is encapsulating multiple hotspots. To correct this problem, you can add in functionality to allow hotspots to keep track of the number of other hotspots they represent.

First, you need to go to your Hotspot class and add in a mutable array property, places. You also add a few method definitions that you can use shortly to help manage this array. The following lines need to be added to Hotspot.h:

//
//  Hotspot.h
//  Recipe 6.6: Grouping Annotations Dynamically
//

#import <MapKit/MapKit.h>

@interface Hotspot : NSObject<MKAnnotation>
{
    CLLocationCoordinate2D _coordinate;
    NSString *_title;
    NSString *_subtitle;
}

@property (nonatomic) CLLocationCoordinate2D coordinate;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subtitle;

@property (nonatomic, strong) NSMutableArray *places;

-(void)addPlace:(Hotspot *)hotspot;
-(int)placesCount;

-(id)initWithCoordinate:(CLLocationCoordinate2D)coordinate title:(NSString *)title subtitle:(NSString *)subtitle;

@end

Not only do you need to implement these methods, but you also need to change your –initWithCoordinate:title:subtitle: method to ensure that your places array is correctly created. You also have to change the title property’s getter, so that the callout title shows the number of hotspots represented. Your implementation file now looks like the following (some unchanged getters and setters have been removed for brevity):

//
//  Hotspot.m
//  Recipe 6.6: Grouping Annotations Dynamically
//

#import "Hotspot.h"

@implementation Hotspot

-(id)initWithCoordinate:(CLLocationCoordinate2D)coordinate title:(NSString *)title subtitle:(NSString *)subtitle
{
    self = [super init];
    if (self) {
        self.coordinate = coordinate;
        self.title = title;
        self.subtitle = subtitle;
        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 is used simply to reset the places array whenever you regroup your annotations. All you have to do now is to add the following two lines to the group: method:

-(void)group:(NSArray *)annotations
{
    float latDelta = self.mapView.region.span.latitudeDelta / scaleLat;
    float longDelta = self.mapView.region.span.longitudeDelta / scaleLong;
    [_annotations makeObjectsPerformSelector:@selector(cleanPlaces)];
    NSMutableArray *visibleAnnotations = [[NSMutableArray alloc] initWithCapacity:0];
    for (Hotspot *current in annotations)
    {
        CLLocationDegrees lat = current.coordinate.latitude;
        CLLocationDegrees longi = current.coordinate.longitude;
        bool found = FALSE;
        for (Hotspot *temp in visibleAnnotations)
        {
            if(fabs(temp.coordinate.latitude - lat) < latDelta &&
               fabs(temp.coordinate.longitude - longi) < longDelta)
            {
                [self.mapView removeAnnotation:current];
                found = TRUE;
                [temp addPlace:current];
                break;
            }
        }
        if (!found)
        {
            [visibleAnnotations addObject:current];
            [self.mapView addAnnotation:current];
        }
    }
}

Now you have a fairly easy way to determine whether any given hotspot is representing any other hotspot, 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, give each hotspot a pointer to its own MKPinAnnotationView. This allows you to control how an annotation is presented based on the number of places it represents. In this case you use this reference to display a red pin for an individual and a green pin for a grouped hotspot.

First, you will add the following property to your Hotspot.h 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 *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
    // if it's the user location, just return nil.
    if ([annotation isKindOfClass:[MKUserLocation class]])
        return nil;
        else
    {
        static NSString *startPinId = @"StartPinIdentifier";
        MKPinAnnotationView *startPin =
           (id)[mapView dequeueReusableAnnotationViewWithIdentifier:startPinId];
                if (startPin == nil)
        {
            startPin = [[MKPinAnnotationView alloc]
                        initWithAnnotation:annotation reuseIdentifier:startPinId];
            startPin.canShowCallout = YES;
            startPin.animatesDrop = YES;
            Hotspot *place = annotation;
            place.annotationView = startPin;
            if ([place placesCount] > 1)
            {
                startPin.pinColor = MKPinAnnotationColorGreen;
            }
            else if ([place placesCount] == 1)
            {
                startPin.pinColor = MKPinAnnotationColorRed;
            }

        }

        return startPin;
    }
}

This makes all 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 problem, you add code to the mapView:regionDidChangeAnimated: method to change the pin color based on the number of places represented, as shown here:

-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
    if (_zoomLevel != mapView.region.span.longitudeDelta)
    {
        [self group:_annotations];
        _zoomLevel = mapView.region.span.longitudeDelta;
        NSSet *visibleAnnotations =
            [mapView annotationsInMapRect:mapView.visibleMapRect];
        for (Hotspot *place in visibleAnnotations)
        {
            if ([place placesCount] > 1)
                place.annotationView.pinColor = MKPinAnnotationColorGreen;
            else
                place.annotationView.pinColor = MKPinAnnotationColorRed;
        }

    }

}

Now, any pins that represent groups of hotspots are green, while individual ones are red, as demonstrated in Figure 6-15.

9781430245995_Fig06-15.jpg

Figure 6-15.  Grouped annotations with number-specific colors

Recipe 6-7: Starting Maps from Your App

In iOS 6, a new API called MKMapItem is available that makes it easy to interact with the built-in Maps app. Instead of building your own half-baked map features you can now, with only a couple of lines of code, turn your users over to the one app that specializes in providing maps and directions. For many apps, this makes perfect sense. After all, map support is a nice feature but not the main focus for most apps.

Although it was possible to launch Maps from within your app before iOS 6, doing so required somewhat ugly URL (uniform resource locator) hacking. The new API is native Objective-C and it allows you to do more than what was possible before. Recipe 6-7 shows you the main ingredients.

Let’s build a really simple application with three buttons that start Maps in different ways. Start by creating a new single-view application and link the Map Kit and the Core Location frameworks to it. Then add three buttons with the titles “Start Maps With One Placemark,” “Start Maps With Multiple Placemarks,” and “Start Maps in Directions Mode,” respectively, to the view controller’s user interface. Your user interface should resemble the one in Figure 6-16.

9781430245995_Fig06-16.jpg

Figure 6-16.  User interface for starting Maps in three ways

Now create actions for each of the three buttons. Name the actions startWithOnePlacemark, startWithMultiplePlacemarks, and startInDirectionsMode, respectively.

Adding Map Items

We’ll start with the simplest case, launching Maps with a single map item. We’ll show you the steps first and then the complete implementation of the startWithOnePlacemark: action method.

First, create a new map item for the location of the famous Big Ben in London. A map item encapsulates a placemark that in turn represents a location coordinate, so start by defining the coordinate.

CLLocationCoordinate2D bigBenLocation = CLLocationCoordinate2DMake(51.50065200, -0.12483300);

Then we create the placemark.

MKPlacemark *bigBenPlacemark = [[MKPlacemark alloc] initWithCoordinate:bigBenLocation addressDictionary:nil];

The address dictionary can be used to provide address information for the placemark to the Maps app. Keep it simple though and send in nil. With the placemark you are ready to create the map item that you will send to the Maps app later.

MKMapItem *bigBenItem = [[MKMapItem alloc] initWithPlacemark:bigBenPlacemark];
bigBenItem.name = @"Big Ben";

Besides the address dictionary of the placemark object, MKMapItem has properties for providing three additional pieces of information associated with the map item: name, phone, and URL. For Recipe 6-7, name is sufficient.

Note  Often, you’ll be dealing with placemarks that you receive from the Core Location framework. These placemarks have a different class (CLPlacemark), than the one in the Map Kit framework (MKPlacemark). However, you can use the initWithPlacemark: method to initialize a Map Kit placemark with one from core location.

Finally, ask Maps to launch with your map item using the openInMapsWithLaunchOptions: method.

[bigBenItem openInMapsWithLaunchOptions:nil];

With all the pieces together, the complete action method looks like the following:

- (IBAction)startWithOnePlacemark:(id)sender
{
    CLLocationCoordinate2D bigBenLocation = CLLocationCoordinate2DMake(51.50065200, -0.12483300);
    MKPlacemark *bigBenPlacemark = [[MKPlacemark alloc] initWithCoordinate:bigBenLocation addressDictionary:nil];
    MKMapItem *bigBenItem = [[MKMapItem alloc] initWithPlacemark:bigBenPlacemark];
    bigBenItem.name = @"Big Ben";

    [bigBenItem openInMapsWithLaunchOptions:nil];
}

If you build and run now you can press the first button and launch Maps with a pin showing the location of the famous clock tower of London, as in Figure 6-17.

9781430245995_Fig06-17.jpg

Figure 6-17.  Maps launched showing Big Ben

When launching Maps with multiple map items you’ll need to use the openMapsWithItems:launchOptions: class method of MKMapItem. It takes an array with map items, but the rest is the same.

Go to the next action method, startWithMultiplePlacemarks, and implement the following:

- (IBAction)startWithMultiplePlacemarks:(id)sender
{
    CLLocationCoordinate2D bigBenLocation = CLLocationCoordinate2DMake(51.50065200, -0.12483300);
    MKPlacemark *bigBenPlacemark = [[MKPlacemark alloc] initWithCoordinate:bigBenLocation addressDictionary:nil];
    MKMapItem *bigBenItem = [[MKMapItem alloc] initWithPlacemark:bigBenPlacemark];
    bigBenItem.name = @"Big Ben";

    CLLocationCoordinate2D westminsterLocation = CLLocationCoordinate2DMake(51.50054300, -0.13570200);
    MKPlacemark *westminsterPlacemark = [[MKPlacemark alloc] initWithCoordinate:westminsterLocation addressDictionary:nil];
    MKMapItem *westminsterItem = [[MKMapItem alloc] initWithPlacemark:westminsterPlacemark];
    westminsterItem.name = @"Westminster Abbey";
    NSArray *items = [[NSArray alloc] initWithObjects:bigBenItem, westminsterItem, nil];
    [MKMapItem openMapsWithItems:items launchOptions:nil];

}

If you build and run now you can see two pins, one for Big Ben and one for Westminster Abbey, as in Figure 6-18.

9781430245995_Fig06-18.jpg

Figure 6-18.  Maps launched with two map items

Launching in Directions Mode

A brand-new feature of iOS 6 is providing the user with turn-by-turn directions in the Maps app. From there, the user can select any placemark and ask Maps how to get there by car or by foot. It is possible for your app to employ this great feature to provide value to your users in a more direct way by launching Maps in directions mode. Let’s go ahead and do that in the last action.

To start Maps in directions mode, all you need to do is to provide an options dictionary with the MKLaunchOptionsDirectionsModeKey set to either MKLaunchOptionsDirectionsModeWalking or MKLaunchOptionsDirectionsModeDriving. The following code launches Maps in directions mode, showing the walking path between Westminster Abbey and Big Ben, as in Figure 6-19. Add it to the startInDirectionsMode action method so that it gets triggered when the user hits the third button in your app.

9781430245995_Fig06-19.jpg

Figure 6-19.  Maps launched in directions mode

- (IBAction)startInDirectionsMode:(id)sender
{
    CLLocationCoordinate2D bigBenLocation =
        CLLocationCoordinate2DMake(51.50065200, -0.12483300);
    MKPlacemark *bigBenPlacemark = [[MKPlacemark alloc]
        initWithCoordinate:bigBenLocation addressDictionary:nil];
    MKMapItem *bigBenItem = [[MKMapItem alloc] initWithPlacemark:bigBenPlacemark];
    bigBenItem.name = @"Big Ben";
    CLLocationCoordinate2D westminsterLocation =
        CLLocationCoordinate2DMake(51.50054300, -0.13570200);
    MKPlacemark *westminsterPlacemark = [[MKPlacemark alloc]
        initWithCoordinate:westminsterLocation addressDictionary:nil];
    MKMapItem *westminsterItem = [[MKMapItem alloc]
        initWithPlacemark:westminsterPlacemark];
    westminsterItem.name = @"Westminster Abbey";

    NSArray *items = [[NSArray alloc] initWithObjects:bigBenItem, westminsterItem, nil];
    NSDictionary *options =
        @{MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeWalking};

    [MKMapItem openMapsWithItems:items launchOptions:options];
}

When launching in directions mode, Maps takes the first item in the array as the starting point and the last as the destination. However, if the array contains only one placemark, Maps will consider that to be the destination, and the device’s current location as the starting point.

But what if you want to find the route from a point to your current location? That’s possible too. MKMapItem provides a way to create a symbolic map item that points out the current location. All you have to do is to add that map to the end of the array of map items. Here’s an example that asks Maps for directions from Big Ben to the current location:

NSArray *items = [[NSArray alloc]
    initWithObjects:bigBenItem, [MKMapItem mapItemForCurrentLocation], nil];
[MKMapItem openMapsWithItems:items launchOptions:nil];

Before finishing this recipe we want to point out that there are more options that can control how Maps will launch—for example, in satellite map mode, or with a specific region. Please refer to Apple’s documentation for details about these and other Maps launch options.

Recipe 6-8: Registering a Routing App

The previous recipe used a brand-new API to launch Maps directly from your app. Another new feature of iOS 6 is that it’s possible to go the other way around; if your app is a registered Routing app, it may be launched from within Maps.

As an example of how this may work, consider a user who has localized Big Ben as a point of interest in Maps. She now wants to know how to get there by bus, so she presses the directions button and selects the routing mode. She then browses through available Routing apps and discovers one that seems to fit her needs. She selects it and the Routing app is launched by Maps.

Recipe 6-8 shows you how you can register your app as a Routing app.

Declaring a Routing App

For Recipe 6-8 we’ll just create a simple dummy app that does nothing more than display the starting point and destination point provided by Maps upon launch. So start by creating a new single-view application and link Map Kit and Core Location frameworks to it.

Next, add a label to the user interface. Make it big enough to contain at least five rows of text. Your main view should look something like the one in Figure 6-20. Also, create an outlet named routingLabel that’s connected to the label.

9781430245995_Fig06-20.jpg

Figure 6-20.  A dummy Routing app with a single label

To allow your app to be launchable from Maps you need to declare it as a Routing app. This is done in the application’s property list file, but there’s a convenient user interface for it in Xcode.

Select the root node in the Project navigator, select the Summary tab, and scroll down to the Maps section. Now check the Enable Directions control, and because this app doesn’t support any of the available transportation types, select Other. (See Figure 6-21.)

9781430245995_Fig06-21.jpg

Figure 6-21.  Declaring a Routing app

Handling Launches

Now that your app is registered as a Routing app, Maps may integrate with it through URL requests. To respond to such a request, add the application:openURL:sourceApplication:annotation: delegate method to your app delegate. Make sure the request is a directions request by using the isDirectionsRequestURL: convenience method of the MKDirectionsRequest class, like so:

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    if ([MKDirectionsRequest isDirectionsRequestURL:url])
    {
        // Code to handle request goes here
        return YES;
    }

    return NO;
}

The request contains a starting point and an end point that your app can use to adjust to the user’s needs. You extract the request from the URL using the initWithContentsOfURL: method.

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    if ([MKDirectionsRequest isDirectionsRequestURL:url])
    {
        MKDirectionsRequest *request = [[MKDirectionsRequest alloc]
            initWithContentsOfURL:url];

        MKMapItem *source = [request source];
        MKMapItem *destination = [request destination];

        return YES;
    }
    return NO;
}

For the purpose of Recipe 6-8 we’ll simply display these points in the routing label of our application. One important aspect is that any of the provided map items may be the symbolic Current Location item, which won’t have an actual placemark attached. You can use the isCurrentLocation property to detect whether that is the case and take appropriate action. In this case you’ll just display the text “Current Location.”

Here’s the final launch response:

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    if ([MKDirectionsRequest isDirectionsRequestURL:url])
    {
        MKDirectionsRequest *request = [[MKDirectionsRequest alloc]
            initWithContentsOfURL:url];
        MKMapItem *source = [request source];
        MKMapItem *destination = [request destination];
        NSString *sourceString;
        NSString *destinationString;
        if (source.isCurrentLocation)
            sourceString = @"Current Location";
        else
            sourceString = [NSString stringWithFormat:@"%f, %f",
                            source.placemark.location.coordinate.latitude,
                            source.placemark.location.coordinate.longitude];
        if (destination.isCurrentLocation)
            sourceString = @"Current Location";
        else
            destinationString = [NSString stringWithFormat:@"%f, %f",
                                destination.placemark.location.coordinate.latitude,
                                destination.placemark.location.coordinate.longitude];
        self.viewController.routingLabel.text =
            [NSString stringWithFormat:@"Start at: %@ Stop at: %@",
             sourceString, destinationString];

        return YES;
    }
    return NO;
}

Testing the Routing App

It’s time to take your app on a test run. Build and run it in the simulator. When your app launches, click on the home button to close the app. You are going to test a routing between Westminster Abbey and Big Ben in London, so start by setting the current location of the simulator to the coordinates of Westminster Abbey. This can be done in the main menu of the simulator, under Debug image Location image Custom Location...,Enter 51.500543 for Latitude and -0.135702 for Longitude in the dialog (see Figure 6-22).

9781430245995_Fig06-22.jpg

Figure 6-22.  Setting a custom location for the simulator

When the custom location has been set, locate and launch the Maps app on the simulator. Enter “Big Ben, London” in the search field and let the Maps app find it. You should eventually see a screen like the one in Figure 6-23 (you’ll need to pan a little to the left to get Westminster Abbey within the visible region).

9781430245995_Fig06-23.jpg

Figure 6-23.  The simulator with Big Ben point of interest selected and current location dot at Westminster Abbey

With the Big Ben placemark selected, click the Directions button located to the left of the search field at the top of the Maps app screen, as shown in Figure 6-24.

9781430245995_Fig06-24.jpg

Figure 6-24.  The Directions button of Maps

In the Directions screen, select the Routing apps mode. This is the button that looks like a bus, next to the walking mode button (see Figure 6-25). With the Routing apps mode selected, click the Routing button on the top right-hand side of the screen.

9781430245995_Fig06-25.jpg

Figure 6-25.  Maps Directions screen with the Routing app mode selected

You’re now presented with a screen of available Routing apps. If everything is set up correctly, your app should be in the list with a Route button next to it, as in Figure 6-26.

9781430245995_Fig06-26.jpg

Figure 6-26.  Your app as a routing option in Maps

Now, if you tap the Route button, your app will be launched and if you’ve implemented the application:openURL:sourceApplication:annotation: method correctly, your app should look like the one in Figure 6-27.

9781430245995_Fig06-27.jpg

Figure 6-27.  Your Routing app launched from within Maps

Specifying Coverage Area

Even though your Routing app successfully integrates with the Maps app, there’s actually one essential piece missing; you need to tell Maps in which region your app provides the routing service. This is done using a special file, a GeoJSON file, to declare the geographic coverage area. Maps uses the information to filter among the available Routing apps so that the user won’t be flooded with irrelevant choices.

The reason it worked for you without the GeoJSON file is that for testing purposes, all Routing apps installed on the simulator are available and considered valid. However, an app cannot be approved for the App Store without submitting a valid GeoJSON file.

Now create a GeoJSON file for your app. Add a file to the Supporting Files folder in the Project Navigator. Pick the GeoJSON template under the Resource section (see Figure 6-28). Name the file London.geojson. You don’t need to add it to the target because it should be submitted with your app and not as part of its bundle.

9781430245995_Fig06-28.jpg

Figure 6-28.  You can use the GeoJSON template to create a new GeoJSON file

Make the content of the new file as follows:

{
    "type": "MultiPolygon",
    "coordinates": [
                    [[[52.257770, -0.989542],
                      [51.001232, -0.943830],
                      [51.050521, 0.303471],
                      [51.848169, 0.362244],
                      [52.257770, -0.989542]]]
                    ]
}

The numbers in the file represent world coordinates and together they make a closed polygon. Because the polygon must be closed, the first and the last coordinate must also be the same.

You can test your GeoJSON file by pointing it out in your Xcode Scheme for the project. Go to Product image Edit Scheme . . . in the menu. In the Options page there’s a setting for Routing App Coverage file. Click on it and select your GeoJSON file. (See Figure 6-29.)

9781430245995_Fig06-29.jpg

Figure 6-29.  Setting the Routing App Coverage file for testing purposes

You now can test whether your file works by selecting points of interests in or out of the coverage area. Be sure to have both the starting and the ending points within the coverage area if you want to test whether your app shows up in the list of available Routing apps.

Note  If your app doesn’t show up as a choice for routing, as you expect it to, then there might be an error in your GeoJSON file. Check the Console because Maps reports any errors with the GeoJSON file there.

A final note before closing this recipe. When you design your own GeoJSON file, we recommend that you keep it simple. Apple suggests no more than 20 polygons containing at most 20 points each. There is no need to be exact, so a simple bounding rectangle will do in most cases.

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 seen the major capabilities of Map Kit, from locating the user to adding annotations and overlays to the new interaction possibilities with Maps. 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 documentation1 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.

1 http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKit_Framework_Reference.

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

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