Chapter     7

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 backend with a map engine of its own. With the introduction of maps in iOS 6, there were many improvements, including a new cartography that provides great-looking maps at any zoom level, better zooming experience thanks to seamless rendering, and, of course, turn-by-turn navigation.

Apple built upon the maps tools in iOS 7 to give you many more ways in which to create submersive mapping applications. Now you have access to the 3-D APIs used in the Maps application to create your own 3-D mapping applications. Overlays have been improved to allow better readability of content, and a new class has been created for requesting direction-related routes from Apple. In this chapter, we will cover these new features as well as many common, real-world mapping situations.

Recipe 7-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 learn how to create an app with a map and how to 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 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 (refer to Chapter 5, Figure 5-2). 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 7-3 provides an example of this system alert box.

Note   Descriptions of 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 Main.storyboard from the navigation pane and drag a map view from the object library onto the view. 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 (such as white). You will also need to make the width of the label larger. Your user interface should now look something like the one in Figure 7-1.

9781430259596_Fig07-01.jpg

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

To make sure the view resizes properly when switching between a 3.5" screen and a 4" screen, select both the MKMapView and the label by command-clicking them and choosing “Add Missing Constraints” from the Resolve Auto Layout Issues menu, which can be found at the bottom of the storyboard (see Chapter 3, Figure 3-7). You should see that the constraint lines are added, as shown in Figure 7-2.

9781430259596_Fig07-02.jpg

Figure 7-2. Main view controller with auto layout constraints added

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. We’ll take care of that next.

Note   Chapter 1 provides detailed instructions about 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 and CoreLocation/CoreLocation.h frameworks libraries to the class with import statements, 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 Listing 7-1, with the foregoing changes in bold.

Listing 7-1.  Adding import statements and declaring MKMapViewDelegate in ViewController.h

//
//  ViewController.h
//  Recipe 7-1 Showing a Map with the Current Location
//

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import <CoreLocation/CoreLocation.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, as shown in Listing 7-2.

Listing 7-2.  Settting the mapView delegate to the viewController

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 surrounding the center coordinate to be shown.

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, as shown in Listing 7-3. In this recipe, you start with a region of 10 by 10 kilometers over Denver, Colorado, in the United States.

Listing 7-3.  Creating the mapView region

// Set initial region
CLLocationCoordinate2D denverLocation = CLLocationCoordinate2DMake(39.739, -104.984);
self.mapView.region =
    MKCoordinateRegionMakeWithDistance(denverLocation, 10000, 10000);

Two optional properties worth mentioning are zoomEnabled and scrollEnabled. These control whether a user can zoom or pan the map, respectively (shown in Listing 7-4).

Listing 7-4.  Optional properties for zoom and scroll

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

Finally, define the map as showing the user’s location. This is easily done by setting the showUserLocation property to “YES.” However, you should set this property only if location services are enabled on the device, as shown in Listing 7-5.

Listing 7-5.  Checking for location services and showing the user location

//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, who is 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 by 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: The 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: The map is panned to keep the user’s location at the center, and the map is 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. Set the user tracking mode, as shown in Listing 7-6.

Listing 7-6.  Setting the user tracking mode

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

Your viewDidLoad method should now resemble Listing 7-7, which combines Listing 7-2 to 7-6.

Listing 7-7.  The completed viewDidLoad method

- (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 that you add to your view controller. Your implementation of the method should look like Listing 7-8.

Listing 7-8.  Implementing the mapView:didUpdateUserLocation: method

-(void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
{
    self.userLocationLabel.text =
        [NSString stringWithFormat:@" 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 location. Figure 7-3 shows your application displaying this prompt. Note that the message includes the location usage description if you provided it in the .plist file.

9781430259596_Fig07-03.jpg

Figure 7-3. 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 from the simulator by choosing option Debug arrow.jpg Location arrow.jpg Freeway Drive. This starts the location simulation services on the simulator, showing a map that should pan to the new location. Because you chose freeway drive, you will see the location move as if you were driving in California near Apple. You can, of course, choose one of the other location simulations if you want. See “Testing Location Updates” in Chapter 4 for more information.

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 provided a 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, add a toolbar to your user interface. Select both the map view and the location label and clear auto layout constraints by selecting “Clear Constraints” in the Resolve Auto Layout Issues menu. From the same menu, choose “add missing constraints” with the toolbar, label, and map view selected. 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 will add a button programmatically in just a moment, but for now your user interface should resemble Figure 7-4.

9781430259596_Fig07-04.jpg

Figure 7-4. Adding a toolbar to the bottom of the view

Now you will add the MKUserTrackingBarButtonItem in code. Switch to the view controller’s implementation file and scroll to the viewDidLoad method. Add the code in Listing 7-9 to the bottom of the method.

Listing 7-9.  Adding MKUserTrackingBarButtonItem to viewDidLoad 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 addition, users can manually pan the map and get back to tracking their location with a tap of the new bar button. Figure 7-5 demonstrates the user-tracking functionality you have implemented.

9781430259596_Fig07-05.jpg

Figure 7-5. Simulated application with panning and user tracking

Recipe 7-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 highlighted points within the map are called annotations. By default, annotations look like pins. This recipe shows you how to add them to your map.

In this recipe, you will build an application similar to the one in Recipe 7-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 won’t 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 from the object library.
  5. Create an outlet for referencing the map view in the viewController.h file. Name the outlet “mapView.”
  6. Import the MapKit framework and make your view controller class conform to the MKMapViewDelegate protocol. The view controller’s header file should now resemble Listing 7-10, with changes from this step in bold.

    Listing 7-10.  The completed ViewController.h file

    //
    //  ViewController.h
    //  Recipe 7-2 Marking Locations wiht Pins
    //

    #import <UIKit/UIKit.h>
    #import <MapKit/MapKit.h>
    #import <CoreLocation/CoreLocation.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, as shown in Listing 7-11.

Listing 7-11.  Initializing the mapView delegate property

- (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 working properly 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 code in Listing 7-12 to the viewDidLoad method.

Listing 7-12.  Creating annotations in 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 7-6. You might need to zoom out to see them; this can be done in the simulator by holding Alt (⌥) to simulate a pinch and dragging outward from the middle of the screen.

9781430259596_Fig07-06.jpg

Figure 7-6. Application with map and pins

In the beginning of this section, we spoke of two objects being necessary to display an annotation. You might be wondering what happened to the second object, the annotation view. We didn’t create one yet, but the annotation objects still 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. In the next section, we will create an annotation view.

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, as shown in Listing 7-13.

Listing 7-13.  Implementing the mapView:viewForAnnotation: method

- (MKAnnotationView *)mapView:(MKMapView *)mapViewviewForAnnotation:(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, is also sent to this method, so you need to make sure the provided annotation is of the type you expect. Add the bold code in Listing 7-14 to the mapView:viewForAnnotation: method to check for this.

Listing 7-14.  Checking for the correct annotation type

- (MKAnnotationView *)mapView:(MKMapView *)mapViewviewForAnnotation:(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 shown in Listing 7-15 to cache the views looks a lot like the code used to create cells for table views (see Chapter 4).

Listing 7-15.  Adding code for caching annotation views in the mapView:viewForAnnotation: method

- (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 might 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 7-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 7-3.

Recipe 7-3: Creating Custom Annotations

Most of the time the default MKPinAnnotationView objects are incredibly useful, but you might at some point decide you want a different image instead of a pin to represent an annotation on your map. Likewise, you might want to display more usable and attractive callouts when the user taps your annotations. To create a custom annotation view, you will be subclassing the MKAnnotationView class. Using Recipe 7-3, you also will 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 first must create your project the same way you did in the preceding recipes. Follow these steps to set up your app skeleton:

  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 will not 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 from the object library.
  5. Create an outlet for referencing the map view from the object library. Name the outlet “mapView.”
  6. Import the Map Kit API in ViewController.h.
  7. Import the MapKit framework and make your view controller class conform to the MKMapViewDelegate protocol. The view controller’s header file should now look like Listing 7-16, with changes from this step in bold.

    Listing 7-16.  The finished ViewController.h file

    //
    //  ViewController.h
    //  Recipe 7-3 Creating Custom 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 7-3 we have chosen a small image, overlay.png, shown here in Figure 7-7. You can get this image from the source code download available on the Apress website page for this book. You can, of course, pick any image you like.

9781430259596_Fig07-07.jpg

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

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. Now make changes to MyAnnotation.h, as shown in Listing 7-17.

Listing 7-17.  Adding a custom initializer and contact info property to the MyAnnotation.h file

//
//  MyAnnotation.h
//  Recipe 7-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

Listing 7-18 shows the corresponding changes to MyAnnotation.m.

Listing 7-18.  Implementing the custom initializer and setting properties

//
//  MyAnnotation.m
//  Recipe 7-3 Creating Custom 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

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 with “MKAnnotationView" as the parent class.

The only thing you do in the custom annotation view class is to override the initWithAnnotation:resuseIdentifier: method. That’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, as shown in Listing 7-19.

Listing 7-19.  Automatically generated code in MyAnnotationView.m

// ...

@implementation MyAnnotationView

- (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. The drawRect: method 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.

Add the code in Listing 7-20 to replace the code that was provided for you. This code creates the custom annotation image to be used instead of the pin. Also, the frame of the annotation view is adjusted to the 40 by 40 points that the image is scaled down to.

Listing 7-20.  MyAnnotationView with the added custom initializer

//
//  MyAnnotationView.m
//  Recipe 7-3 Creating Custom 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(0, -20);
    }
    return self;
}

@end

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. As you can see in Listing 7-20, an offset was created for this example using CGMake(0,-20), which moved the relative position of the image up by 20 points. This was necessary to align the point of the image to the coordinate.

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

Listing 7-21.  Implementing the map delegate method

//
//  ViewController.m
//  Recipe 7-3 Creating Custom 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 bold lines in Listing 7-22 to create a couple of annotations and add them to your map.

Listing 7-22.  Creating some test data

@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 7-8 provides a simulation of this app.

9781430259596_Fig07-08.jpg

Figure 7-8. 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 side of the callout. You’ll use it later to display a detailed view of the annotation.

Return to MyAnnotationView.m and extend the initWithAnnotation:reuseidentifier: method with the code shown in Listing 7-23.

Listing 7-23.  Modifying the annotation view in the custom initializer

- (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 an image view to scale it down, this time to 20 by 20 points.

If you build and run your application now, your annotations will present callouts such as the one in Figure 7-9.

9781430259596_Fig07-09.jpg

Figure 7-9. Map with custom annotations, one of which is showing a callout

Adding a Detailed View

At this point, your callouts are 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. In the Apple Maps application you can see a similar behavior when you select a business that has been provided by a map search. Selecting the info button gives you details about the business, such as the address and rating.

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.

For this example, we’re going to use a mix of storyboards and .xib view files. Start by creating the new class. Name the new class DetailedViewController and make sure it’s a subclass of UIViewController. On the file options screen, select the “With XIB for user interface” option. This will create a new class, and a .xib file will now appear with the same class name.

Select the .xib file of DetailedViewController from the project navigator and add three labels to the provided view. Place them near the bottom of the view, as shown in Figure 7-10.

9781430259596_Fig07-10.jpg

Figure 7-10. DetailedViewController.xib view

Now, create outlets for the three labels. Name them titleLabel, subtitleLabel, and contactInformationLabel, respectively. Also, make additions to the header file, as shown in Listing 7-24.

Listing 7-24.  The DetailedViewController.h implementation

//
//  DetailedViewController.h
//  Recipe 7-3 Creating Custom 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 *contactInformationLabel;

@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, as shown in Listing 7-25.

Listing 7-25.  Implementing the custom initializer in the DetailViewController.m file

//
//  DetailedViewController.m
//  Recipe 7-3 Creating Custom Annotations
//

// ...

@implementation DetailedViewController

// ...

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

// ...

@end

In the viewDidLoad method of the detailed view controller, add code to initialize the labels with texts from the stored annotation object, as shown in Listing 7-26.

Listing 7-26.  The viewDidLoad implementation

- (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.contactInformationLabel.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 this recipe we chose the partial curl transition, which is a pretty cool effect. Listing 7-27 shows how you set it up. Don’t forget to import the DetailedViewController into the ViewController.m class.

Listing 7-27.  Implementing the mapView:annotationView:calloutAccessoryControlTapped: method

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

@interface ViewController ()
//...

-(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:^{}];
}

@end

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

9781430259596_Fig07-11.jpg

Figure 7-11. Application responding to the tapping of callouts

Recipe 7-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 preceding 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. Import the MapKit framework and make your view controller class conform to the MKMapViewDelegate protocol. The view controller’s header file should now look like Listing 7-28.

    Listing 7-28.  The finished ViewController.h file

    //
    //  ViewController.h
    //  Recipe 7-4 Dragging a Pin
    //

    #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, as shown in Listing 7-29.

Listing 7-29.  Initializing the delegate property in the viewDidLoad method

- (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 in the map when the application has loaded the main view by modifying the viewDIdLoad method, as shown in Listing 7-30.

Listing 7-30.  Placing a pin on the map when the application loads

- (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 Denver, 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, as shown in Listing 7-31. The code is nearly identical to the one used in Recipes 7-2 and 7-3, except that you now set the draggable property.

Listing 7-31.  The mapView:viewForAnnotation: method implementation

- (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 was 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 Denver to any other place of your choice. However, let’s turn this cool but somewhat useless app into a tool. You will intercept when the user drops the pin and output the new location to the console. To do this you will make use of the mapView:annotationView:didChangeDragState:fromOldState: delegate method. The method name is long but self-explanatory. Add Listing 7-32 to your view controller.

Listing 7-32.  Implementing the delegate for detecting the changed state of a pin

-(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 coordinates of the pin, as in Figure 7-12. The tool can be quite useful if you want to create some location test data of your own.

9781430259596_Fig07-12.jpg

Figure 7-12. The tool prints new pin locations to the console

Not bad for a few lines of code. Now let’s move on to adding overlays in a map.

Recipe 7-5: Adding Overlays to a Map

An annotation, as you’ve seen in the preceding 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 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 as you did with annotations in Recipe 7-3.

Again, start by setting up a new map-based application. We trust you know the steps by now, but you could always refer to the preceding recipes for guidance.

Creating the Overlays

Again, you will create your test data in the viewDidLoad method of your view controller. First is a circle overlay over large parts of Mexico. Add Listing 7-33 to the viewDidLoad method.

Listing 7-33.  Creating a circle overlay

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

Next, create a polygon overlay. Note that a polygon must always start and end in the same location. Add Listing 7-34 to the viewDidLoad method.

Listing 7-34.  Creating a polygon overlay

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];

Add a line overlay to the viewDidLoad method. Listing 7-35 shows this code.

Listing 7-35.  Creating 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. In iOS 7, Apple provides a new feature with overlays. Now you can specify a level. You have two options for the overlays: they are drawn either above the roads or above the roads and labels. You can use the properties MKOverlayLevelAboveRoads or MKOverlayLevelAboveLabels, respectively. In our example, we’ll put the label above the roads so the text is easier to read; in other words, the labels will be above the overlay. Add the code in Listing 7-36 to the viewDidLoad method to do this.

Listing 7-36.  Adding the overlays to the map above the roads but below the labels

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

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

Listing 7-37.  Implementing the mapView:rendererForOverlay: delegate method

-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id )overlay
{
    if([overlay isKindOfClass:[MKCircle class]])
    {
        MKCircleRenderer *renderer = [[MKCircleRenderer alloc] initWithOverlay:overlay];
        
        //Display settings
        renderer.lineWidth = 1;
        renderer.strokeColor = [UIColor blueColor];
        renderer.fillColor = [[UIColor blueColor] colorWithAlphaComponent:0.5];
        return renderer;
    }
    if([overlay isKindOfClass:[MKPolygon class]])
    {
        MKPolygonRenderer *renderer= [[MKPolygonRenderer alloc] initWithOverlay:overlay];
        
        //Display settings
        renderer.lineWidth=1;
        renderer.strokeColor=[UIColor blueColor];
        renderer.fillColor=[[UIColor blueColor] colorWithAlphaComponent:0.5];
        return renderer;
    }
    else if ([overlay isKindOfClass:[MKPolyline class]])
    {
        MKPolylineRenderer *renderer = [[MKPolylineRenderer alloc] initWithOverlay:overlay];
        
        //Display settings
        renderer.lineWidth = 3;
        renderer.strokeColor = [UIColor blueColor];
        return renderer;
    }
    
    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 a transparency (alpha) component.

You’re finished with this recipe. When you build and run the application, you should see a screen resembling the one in Figure 7-13.

9781430259596_Fig07-13.jpg

Figure 7-13. An app with a circle, a polygon, and a line overlay

Recipe 7-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. Recipe 7-6 employs 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 7-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 changes shown in bold in Listing 7-38 to your ViewController.h file.

Listing 7-38.  The ViewController.h file with added property and instance variables

//
//  ViewController.h
//  Recipe 7-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 code in Listing 7-39 to instantiate the _annotations array. Give the array an initial capacity of 1,000 objects because you’ll be making that many annotations soon.

Listing 7-39.  Adding code to instantiate the _annotations array

- (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 Listing 7-40.

Listing 7-40.  The completed Hotspot.h file

//
//  Hotspot.h
//  Recipe 7-6 Grouping Annotations Dynamically
//
#import <Foundation/Foundation.h>
#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, as shown in Listing 7-41.

Listing 7-41.  Starting implementation of the Hotspot.m file

//
//  Hotspot.m
//  Recipe 7-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 statements in Listing 7-42 before your import statements in the ViewController.m file.

Listing 7-42.  Constant definitions

#define centerLat 39.7392
#define centerLong -104.9842
#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 (Listing 7-43).

Listing 7-43.  Hotspot.h import statement

#import "Hotspot.h"

Listing 7-44 shows two methods that 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. Add these to the view controller.

Listing 7-44.  Methods to generate 1,000 hotspots

-(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:-103.0 to:-107.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. Listing 7-45 shows the code necessary to do that.

Listing 7-45.  Updating the viewDidLoad method to invoke new methods and add annotations

- (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 you can correctly display your pins. Listing 7-46 is similar to the one used in the preceding recipes.

Listing 7-46.  Implementing the mapView:viewForAnnotation: method

- (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;
    }
}

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

9781430259596_Fig07-14.jpg

Figure 7-14. 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 method in Listing 7-47 provides an efficient implementation and should be placed in your view controller’s .m file.

Listing 7-47.  Method for iterating through annotations and grouping them

-(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 delegate method shown in Listing 7-48.

Listing 7-48.  Implementation of the mapView:regionDidChangeAnimated: 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 7-15. When zooming in or out, you can see annotations appear or disappear, respectively, as the map changes.

9781430259596_Fig07-15.jpg

Figure 7-15. Grouped annotations by location

Adding Color Coding

Your annotations are correctly grouping at this point, but 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 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 a mutable array property—places. You also need to add a few method definitions that you can use shortly to help manage this array. Listing 7-49 shows lines in bold that need to be added to Hotspot.h.

Listing 7-49.  Modification of the Hotspot.h to add method definitions and a mutable array

//
//  Hotspot.h
//  Recipe 7-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;
-(void)cleanPlaces;

-(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 should now look like Listing 7-50 (some unchanged getters and setters have been removed for brevity).

Listing 7-50.  The refactored Hotspot.m file

//
//  Hotspot.m
//  Recipe 7-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 add the two lines to the group: method:, as shown in Listing 7-51.

Listing 7-51.  Modifying the group: method to includes methods for counting and clearing places

-(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, located in the ViewController.m file. Listing 7-52 shows the delegate method implementation.

Listing 7-52.  New implementation of the mapView: viewForAnnotation: method

- (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 individual hotspots. 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, add code to the mapView:regionDidChangeAnimated: method to change the pin color based on the number of places represented, as shown in Listing 7-53.

Listing 7-53.  The updated mapView:regionDidChangeAnimated: method

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

9781430259596_Fig07-16.jpg

Figure 7-16. Grouped annotations with number-specific colors

Recipe 7-7: Starting Maps from Your App

In iO7, an 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.

Let’s build a really simple application with three buttons that start Maps in different ways. Use the following steps to get started:

  1. Start by creating a new single view application and link the Map Kit and the Core Location frameworks to it.
  2. Import the Mapkit framework in the header file.
  3. Add three buttons and configure them to look like Figure 7-17.

    9781430259596_Fig07-17.jpg

    Figure 7-17. User interface for starting Maps in three ways

  4. Create actions for the buttons with the following respective names:
    • Button Action: startWithOnePlacemark
    • Button Action: startWithMultiplePlacemarks
    • Button Action: startInDirectionsMode

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, as shown in Listing 7-54. A map item encapsulates a placemark that in turn represents a location coordinate, so start by defining the coordinate.

Listing 7-54.  Creating a map item for the Big Ben location

CLLocationCoordinate2D bigBenLocation =
    CLLocationCoordinate2DMake(51.50065200, -0.12483300);

Listing 7-55 shows how to create a placemark. 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.

Listing 7-55.   Creating a placemark

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

With the placemark you are ready to create the map item that you will send to the Maps app later. Listing 7-56 shows how to create this. 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 this recipe, the name property is sufficient.

Listing 7-56.  Creating a map item

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

Note   Often, you’ll need to deal with placemarks you receive from the Core Location framework. These placemarks have a different class (CLPlacemark) from 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.

Listing 7-57 shows how to ask Maps to launch with your map item using the openInMapsWithLaunchOptions: method.

Listing 7-57.  Asking Maps to launch with a map item

[bigBenItem openInMapsWithLaunchOptions:nil];

Combining Listing 7-54 to 7-57, the complete action method looks like Listing 7-58.

Listing 7-58.  The complete startWithOnePlacemark: method

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

9781430259596_Fig07-18.jpg

Figure 7-18. 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 it as shown in Listing 7-59.

Listing 7-59.  The startWithMultiplePlacemarks: method implementation

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

9781430259596_Fig07-19.jpg

Figure 7-19. Maps launched with two map items

Launching in Directions Mode

Another cool feature 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 code in Listing 7-60 launches Maps in directions mode, showing the walking path between Westminster Abbey and Big Ben, as in Figure 7-20. Add it to the startInDirectionsMode action method so that it gets triggered when the user hits the third button in your app, as seen in Listing 7-60.

9781430259596_Fig07-20.jpg

Figure 7-20. Maps launched in directions mode

Listing 7-60.  Implementation of the startInDirectionsMode: method

- (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 to be 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. Refer to Apple’s documentation for details about these and other Maps launch options.

Recipe 7-8: Registering a Routing App

The preceding recipe used an API to launch Maps directly from your app. Another feature of iOS 7 is that it’s possible to go the other way around; if your app is a registered routing app, it might be launched from within Maps.

As an example of how this might work, consider users who have localized Big Ben as a point of interest in Maps. They now want to know how to get there by bus, so they press the directions button, select the routing mode, and then browse through available routing apps and discover one that seems to fit their needs. They select it, and the routing app is launched by Maps.

Recipe 7-8 shows you how you can register your app as a routing app.

Declaring a Routing App

For Recipe 7-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 the Map Kit and Core Location frameworks to it. Make sure you import the Map Kit framework in the header file.

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 7-21. Also, create an outlet named “routingLabel” that’s connected to the label.

9781430259596_Fig07-21.jpg

Figure 7-21. 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 Capabilities tab, and scroll down to the Maps section. Make sure the switch is set to “ON” and, because this app doesn’t support any of the available transportation types, select Other. (See Figure 7-22.)

9781430259596_Fig07-22.jpg

Figure 7-22. Declaring a routing app

Handling Launches

Now that your app is registered as a routing app, Maps can 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 isD to your app delegate. Make sure the request is a directions request by using the isDirectionsRequestURL: convenience method of the MKDirectionsRequest class, as shown in Listing 7-61.

Listing 7-61.  Implementation of the application:openURL:sourceApplication:annotation: delegate

- (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. Modify the code in Listing 7-61, as shown in Listing 7-62.

Listing 7-62.  Extracting the request

- (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 this recipe we’ll simply display these points in the routing label of our application. One important aspect is that any of the provided map items might 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.” Listing 7-63 shows the final launch response.

Listing 7-63.  Adding to launch response to code in Listing 7-62

- (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= (ViewController*)self.window.rootViewController;
        self.viewController.routingLabel.text =
            [NSString stringWithFormat:@"Start at: %@ Stop at: %@",
             sourceString, destinationString];
        
        return YES;
    }
    return NO;
}

Let’s not forget to update the AppDelegate.h file to include our imports and a property for the view controller class, as shown in Listing 7-64.

Listing 7-64.  Adding an import statement and a property for the view controller class in the AppDelegate.h file

//
//  AppDelegate.h
//  Recipe 7-8 Registering a Routing App
//
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import "ViewController.h"

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (strong,nonatomic) ViewController *viewController;

@end

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 the “home” button to close the app. You will 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 arrow.jpg Location arrow.jpg Custom Location. Enter 51.500543 for latitude and -0.135702 for longitude in the dialog box (see Figure 7-23).

9781430259596_Fig07-23.jpg

Figure 7-23. 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 7-24 (you’ll need to pan a little to the left to get Westminster Abbey within the visible region).

9781430259596_Fig07-24.jpg

Figure 7-24. The simulator with the Big Ben point of interest selected and the 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 7-25.

9781430259596_Fig07-25.jpg

Figure 7-25. The “Directions” button in Maps app

In the Directions screen, select the “routing apps mode” button. This is the button that looks like a bus, next to the “walking mode” button (see Figure 7-26). With the routing apps mode selected, click the routing button on the upper-right side of the screen. You can also press the list item if it has a bus icon next to it.

9781430259596_Fig07-26.jpg

Figure 7-26. 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 7-27.

9781430259596_Fig07-27.jpg

Figure 7-27. 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 7-28.

9781430259596_Fig07-28.jpg

Figure 7-28. Your routing app launched from within Maps

Specifying a 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 7-29). 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.

9781430259596_Fig07-29.jpg

Figure 7-29. 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 arrow.jpg scheme arrow.jpg Edit Scheme in the menu. In the Options page, there’s a setting for Routing App Coverage file. Click it and select your GeoJSON file. (See Figure 7-30.)

9781430259596_Fig07-30.jpg

Figure 7-30. 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, 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.

Recipe 7-9: Getting Directions

A new API added in iOS 7 lets you get driving or walking directions from within your app. These directions come with everything you would expect from a maps application, including alternate routes and route finding. As an added bonus, you also have time estimates that are based on current traffic and even historical data.

Setting Up the Application

As you have done before in many of the map applications in this chapter, you need to create a new single view application with a map view on it. Here are the instructions once again for your convenience:

  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 will not 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 into ViewController.h.
  7. Add the NSLocationUsageDescription key to the application.info plist file and give it a suitable identifier, such as “Testing Getting Directions.”
  8. Make your view controller class conform to the MKMapViewDelegate protocol.

Drawing Directions on the Map

To start, we will create a new method for finding our directions. In this method, we need to make a request using both our source and destination coordinates. For our example, we have chosen Red Rocks Amphitheatre and the Sports Authority Field in Denver as the source and destination for the directions, respectively. Add the code shown in Listing 7-65 to the ViewController.h and ViewController.m files.

Listing 7-65.  Setting up the viewController.h and .m files

//
//  ViewController.h
//  Recipe 7-9 Getting Directions
//
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import <CoreLocation/CoreLocation.h>

@interface ViewController : UIViewController <MKMapViewDelegate>

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

@end

//
//  ViewController.m
//  Recipe 7-9 Getting Directions
//
//...

-(void)findDirectionsFrom: (MKMapItem *)source to:(MKMapItem *)destination
{
    //Make request and provide it with source and destination MKMapItems
    MKDirectionsRequest *request = [[MKDirectionsRequest alloc] init];
    request.source = source;
    request.destination = destination;
    request.requestsAlternateRoutes = NO;
    
    MKDirections *directions = [[MKDirections alloc] initWithRequest:request];
    
    //Find directions and call a method to show directions
    [directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) {
        if(error)
        {
            NSLog(@"Bummer, we got an error: %@",error);
        }
        else
        {
            [self showDirectionsOnMap:response];
            
        }
    }];
    
}

-(void)showDirectionsOnMap:(MKDirectionsResponse *)response
{
    self.response = response;
    
    for (MKRoute *route in self.response.routes)
    {
        [self.mapView addOverlay:route.polyline level:MKOverlayLevelAboveRoads];
    }

    [self.mapView addAnnotation:self.response.source.placemark];
    [self.mapView addAnnotation:self.response.destination.placemark];

    
}

The two methods here receive a request for directions and then create a route on the map in the form of a polyline, with both the source and destination points marked with a pin. We’ll need to add one more delegate method to the view controller to actually draw the route on the map. You’ll do that by adding the the code shown in Listing 7-66.

Listing 7-66.  Adding the mapView:rendererForOverlay: delegate method

//
//  ViewController.m
//  Recipe 7-9 Getting Directions
//

//...

-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id )overlay
{
    
    if([overlay isKindOfClass:[MKPolyline class]])
    {
        MKPolylineRenderer *renderer = [[MKPolylineRenderer alloc] initWithOverlay:overlay];
        renderer.lineWidth = 3;
        renderer.strokeColor = [UIColor blueColor];
        return renderer;
        
    }
    else
    {
        return nil;
    }
}

Lastly, we need to set some coordinates in the viewDidLoad method and call the newly created methods for finding directions and putting them on the map. First, add some constants to the ViewController.m file above the “import,” as shown in Listing 7-67.

Listing 7-67.  Defining constants

#define centerLat 39.6653
#define centerLong -105.2058
#define spanDeltaLat .5
#define spanDeltaLong .5

#import "ViewController.h"

//...

Then create some MKMapItems using the coordinates for both Red Rocks and Sports Authority and use them to call the findDirectionsFrom: to: method we just created in the viewDidLoad method, as shown in Listing 7-68. As mentioned before, an MKMapItem holds information about a place such as the placemark and the name.

Listing 7-68.  Updating the viewDidLoad method to call the findDirectionsFrom: to: method

- (void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    self.mapView.delegate = self;
    
    CLLocationCoordinate2D centerPoint = {centerLat, centerLong};
    MKCoordinateSpan coordinateSpan = MKCoordinateSpanMake(spanDeltaLat, spanDeltaLong);
    MKCoordinateRegion coordinateRegion =
    MKCoordinateRegionMake(centerPoint, coordinateSpan);
    
    [self.mapView setRegion:coordinateRegion];
    [self.mapView regionThatFits:coordinateRegion];
    
    CLLocationCoordinate2D redRocksAmphitheatre= CLLocationCoordinate2DMake(39.6653, -105.2058);
    MKPlacemark *redRocksPlacemark = [[MKPlacemark alloc] initWithCoordinate: redRocksAmphitheatre addressDictionary:nil];
    MKMapItem *redRocksItem = [[MKMapItem alloc] initWithPlacemark:redRocksPlacemark];
    redRocksItem.name = @"Red Rocks Amphitheatre";
    
    CLLocationCoordinate2D sportsAuthorityField = CLLocationCoordinate2DMake(39.7439, -105.0200);
    MKPlacemark *sportsAuthorityPlacemark = [[MKPlacemark alloc] initWithCoordinate:sportsAuthorityField addressDictionary:nil];
    MKMapItem *sportsAuthorityItem = [[MKMapItem alloc] initWithPlacemark:sportsAuthorityPlacemark];
    sportsAuthorityItem.name = @"Sports Authority Field";
    
    [self findDirectionsFrom:redRocksItem to: sportsAuthorityItem];
    
}

If you build and run your app, you will see a polyline route is drawn from the Red Rocks Amphitheatre to the Sports Authority Field, as shown in Figure 7-31. We didn’t do it in this example, but you can also draw multiple routes by changing request.requestsAlternateRoutes from NO to YES in the findDirectionsFrom: to: method.

9781430259596_Fig07-31.jpg

Figure 7-31. Drawing directions on a map

Adding ETA

Before concluding this recipe, we will explore one more handy feature included in the MKDirections class that allows you to get the estimated time of arrival (ETA). The ETA is based on current traffic conditions and can be a nice feature to add to your mapping applications.

Listing 7-69 shows how to modify the findDirectionsFrom: to: method to add an ETA alert. Basically, all you are doing is calling a class method, calculateETAWithCompletionHandler, inside the calculateDirectionsWIthCompletionHandler: method’s completion handler. You need to do this because the calculateDirectionsWithCompletionHandler: method is carried out asynchronously, and you will get an error if you try to carry out two MKDirections method calls at the same time. By adding the second method to the first method’s completion block, you ensure that the first method has completed first.

Listing 7-69.  Creating an alert view to display ETA

 -(void)findDirectionsFrom: (MKMapItem *)source to:(MKMapItem *)destination
{
//...
    
    [directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) {
        if(error)
        {
            NSLog(@"Bummer, we got an error: %@",error);
            
        }
        else
        {
            [self showDirectionsOnMap:response];
            
            [directions calculateETAWithCompletionHandler:^(MKETAResponse *response, NSError *error)
                {

                NSLog(@"You will arive in: %.1f Mins%@", response.expectedTravelTime/60.0,error);
                
                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"ETA!"
                                                                message:[NSString stringWithFormat:@"Estimated Time of Arrival in: %.1f Mins.",response.expectedTravelTime/60.0]
                                                               delegate:nil
                                                      cancelButtonTitle:@"Dismiss"
                                                      otherButtonTitles: nil];
                [alert show];
            }];
            
        }
    }];
}

That concludes this recipe. If you have done this correctly you will see an alert with the ETA, as shown in Figure 7-32.

9781430259596_Fig07-32.jpg

Figure 7-32. Displaying the ETA in an alert view

Recipe 7-10: Using 3-D Mapping

Before iOS 7, 3-D features were limited solely to the Maps app. Now Apple has given us some new APIs that let us take advantage of 3-D mapping features such as choosing camera position and even animating 3-D views. In this recipe, we will explore two methods of displaying a 3-D map view as well as creating a fly-over.

Using the Properties Approach

As you did in many of the recipes in this chapter, you need to set up a new map-view single view application to get started. The new MKMapCamera API lets you choose a camera location when viewing a map. You can set a camera using either a convenience method or by setting some properties. We’ll start with setting properties, and then we’ll show you how to get the same effect using the convenience method.

The MKMapCamera API actually makes it pretty easy to put your map in a 3-D view. All that is needed is setting four properties and setting the mapView camera to the MKMapCamera object we create. The four properties are as follows:

  • Pitch: This is the angle of the camera lens in degrees. A zero value will look straight at the ground and a nonzero value will be toward the horizon.
  • Altitude: This is the distance in meters above the ground. Generally, if you want to see buildings this should be in the ball park of 600 or less. This value changes depending on the height of the buildings.
  • Heading: This is the degree heading where north is 0 degrees and south is 180 degrees.
  • centerCoordinate: This is a CLLocationCoordinate type input that specifies where the camera is located.

There are also a few properties that can be called on the MKMapView object to show items such as buildings and points of interest as well as allow zooming and pitch. We’ll be using a few of these in our code example.

All that is needed in the recipe is a little modification to the viewDidLoad method. Modify the viewDidLoad method as shown in Listing 7-70.

Listing 7-70.  Modifying the viewDidLoad method to create a 3-D map view

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.mapView.delegate = self;
    
    //Create a new MKMapCamera object
    MKMapCamera *mapCamera = [[MKMapCamera alloc] init];
    
    //set MKMapCamera properties
    mapCamera.centerCoordinate = CLLocationCoordinate2DMake(40.7130,-74.0085);
    mapCamera.pitch = 57;
    mapCamera.altitude = 650;
    mapCamera.heading = 90;
    
    //Set MKmapView camera property
    self.mapView.camera = mapCamera;
    
    //Set a few MKMapView Properties to allow pitch, building view, points of interest, and zooming.
    self.mapView.pitchEnabled = YES;
    self.mapView.showsBuildings = YES;
    self.mapView.showsPointsOfInterest = YES;
    self.mapView.zoomEnabled = YES;

}

The code in Listing 7-70 simply initializes the map-view delegate and then creates an MKMapCamera instance with the needed coordinate, pitch, altitude, and heading. Next, add that camera to the map view and set properties on the map view to make it look and behave better.

Now if you build and run the application, you’ll find a nice building view of the One World Trade Center Building in New York City, as shown in Figure 7-33.

9781430259596_Fig07-33.jpg

Figure 7-33. Using MKMapCamera to view a 3-D map

Using the Convenience Method Approach

The property approach is concise and easy to understand, but there is a faster and easier method for showing a 3-D view. Apple has provided a convenience method that can do all of this when you initialize your MKMapCamera object. The ViewDidLoad method would look like Listing 7-71 instead of Listing 7-70.

Listing 7-71.  Modifying the viewDidLoad method to create a 3-D map view using the convenience method

- (void)viewDidLoad
{
    [super viewDidLoad];
        
    self.mapView.delegate = self;
    

    //Using the Convenience Method
    CLLocationCoordinate2D ground = CLLocationCoordinate2DMake(40.7128,-74.0117);
    CLLocationCoordinate2D eye = CLLocationCoordinate2DMake(40.7132,-74.0150);
    MKMapCamera *mapCamera = [MKMapCamera cameraLookingAtCenterCoordinate:ground fromEyeCoordinate:eye eyeAltitude:740];

    //Set MKmapView camera property
    self.mapView.camera = mapCamera;
    
    //Set a few MKMapView Properties to allow pitch, building view, points of interest, and zooming.
    self.mapView.pitchEnabled = YES;
    self.mapView.showsBuildings = YES;
    self.mapView.showsPointsOfInterest = YES;
    self.mapView.zoomEnabled = YES;

}

The convenience method shown in Listing 7-71 uses two coordinates. The first coordinate is where you want the camera to look and the second coordinate is where you want the camera to be. The last parameter is simply the altitude above the ground. If you build and run this app, you should see a slightly different view of the One World Trade Center Building.

Creating a Fly-Over

Because MKMapView is a type of view, you can actually animate it. You will take advantage of this fact by creating a nice fly-over effect that will move from one camera to another. This is actually very simple to do using the UIView animateWithDuration: method.

To start, you need to create a new MKMapCamera for the second location. For this demonstration, we will use the convenience method to create the MKMapCamera instances. Modify the code from Listing 7-71 to add a new camera instance, as shown in Listing 7-72.

Listing 7-72.  Adding a new MKMapCamera instance to the viewDidLoad method

- (void)viewDidLoad
{
    [super viewDidLoad];
        
    self.mapView.delegate = self;
    

    //Using the Convenience Method
    CLLocationCoordinate2D ground = CLLocationCoordinate2DMake(40.7128,-74.0117);
    CLLocationCoordinate2D eye = CLLocationCoordinate2DMake(40.7132,-74.0150);
    MKMapCamera *mapCamera = [MKMapCamera cameraLookingAtCenterCoordinate:ground fromEyeCoordinate:eye eyeAltitude:740];

 
    CLLocationCoordinate2D ground2 = CLLocationCoordinate2DMake(40.7,-73.99);
    CLLocationCoordinate2D eye2 = CLLocationCoordinate2DMake(40.7,-73.98);
    MKMapCamera *mapCamera2 = [MKMapCamera cameraLookingAtCenterCoordinate:ground2 fromEyeCoordinate:eye2 eyeAltitude:700];

//...
}

In Listing 7-72, you can see that the code added is identical to the implementation of the first camera, except the coordinates are now slightly different. The last step is to create the animation. To make a nice, smooth animation, we’ll cover a short distance over a fair amount of time (25 seconds). Add the code in Listing 7-73 to the viewDidLoad method.

Listing 7-73.  Creating a 25-second animation to the second camera view

[UIView animateWithDuration:25.0 animations:^{

   self.mapView.camera = mapCamera2;

}];

If you build and run the application now, you should see a very nice fly-over from the One World Trade Center Building across the Brooklyn Bridge. We encourage you to play with the coordinates and altitudes to see how it affects the appearance of the animation.

It’s worth pointing out that if you try to move over a distance too fast or if you add too many annotations and details to the map, the animation will be choppy or the items will not load at all. Debugging these issues are beyond the scope of this book, but you can view the “Putting Map Kit in Perspective” video found at https://developer.apple.com/wwdc/videos/ for more details.

Summary

The Map Kit framework is one of the most popular frameworks because of 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 3-D capabilities 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 documentation 1 reveals various other commands, methods, and properties we 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