Chapter 3. Map Kit

The Map Kit framework allows you to embed maps directly into your views, and provides support for annotating these maps and adding overlays. Along with the Core Location framework, it does all the heavy lifting involved in creating and displaying maps in your applications.

Adding a Map

Let’s add a map to the Location application you built in the previous chapter. Open up the application in Xcode. The first thing you need to do is add the Map Kit framework to your project. Click the project icon at the top of the Project pane in Xcode, click the main Target for the project, and then click the Build Phases tab. Finally, click the Link Binary with Libraries item to open up the list of linked frameworks, and click the + symbol to add a new framework. Select the MapKit.framework from the drop-down list and click the Add button to add the framework to your project.

Now click the Supporting Files group in Project navigator, and then click the Location_Prefix.pch file to open it in the standard editor. Instead of having to include the framework every time you need it (as you did in the last chapter), you can use this header file to import it into all the source files in the project, as highlighted in bold below:

#ifdef __OBJC__
    #import <Foundation/Foundation.h>
    #import  <UIKit/UIKit.h>
    #import <CoreLocation/CoreLocation.h>
    #import <MapKit/MapKit.h>
#endif

Once you’ve done that, make sure you’ve saved your changes and click on the ViewController.xib file to open it in Interface Builder. Open the Utility panel if it’s not already open, then drag and drop a Table View Cell (UITableViewCell) from the Object Library into the editor.

Click the Table View Cell to highlight it, and then in the Size Inspector, resize the cell to have W = 300 points (rather than the default cell width of 320 points) and H = 170 points. This will allow the cell to fit easily into the grouped table view mode of our user interface. Next, drag and drop a Map View (MKMapView) from the Object Library and position it inside our newly resized table view cell, as shown in Figure 3-1.

Creating the custom UITableViewCell

Figure 3-1. Creating the custom UITableViewCell

Save your changes and close the Utility panel, then click to open the Assistant editor. Right-click and drag from the UITableViewCell and the MKMapView the table view cell contains into the ViewController.h interface file to create two more properties called mapCell and mapView as an IBOutlet (see Figure 3-2).

Creating the mapCell and mapView properties

Figure 3-2. Creating the mapCell and mapView properties

Save your changes and return to the Standard editor, then click the ViewController.h interface file to open it. You should see something a lot like this:

#import <UIKit/UIKit.h>

@interface ViewController :
    UIViewController <UITableViewDelegate, UITableViewDataSource>

@property (strong, nonatomic) IBOutlet UITableView *tableView;
@property (strong, nonatomic) IBOutlet UITableViewCell *mapCell;
@property (weak, nonatomic) IBOutlet MKMapView *mapView;

@end

Now click the corresponding implementation file, ViewController.m, to open it in the editor. Here you’re going to add a new UITableViewDataSource method:

- (CGFloat)tableView:(UITableView *)tableView
  heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSInteger height;
    if ( indexPath.section == 0 ) {
        height = 170;
    } else {
        height = 44;
    }
    return height;
}

Then, modify both the numberOfSectionsInTableView: and the tableView:numberOfRowsInSection: delegate methods.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
    return 3;
}

- (NSInteger) tableView:(UITableView *)tv
        numberOfRowsInSection:(NSInteger) section {
    if ( section == 0 ) {
        return 1;
    } else if ( section == 1 ) {
        return 5;
    } else {
        return 1;
    }
}

Here you’ve added another section to the grouped table view. The first section has only one cell of height H = 170 pixels, and the second section has five rows of height H = 44 pixels, which is the default height for an iPhone UITableViewCell. The final section has just one cell with the default height. Now you need to modify TableView:cellForRowAtIndexPath: so that you return your customized map cell in the first section, your location table view cells in the second section, and the number of points in your location database file in the final section.

- (UITableViewCell *)tableView:(UITableView *)tv
        cellForRowAtIndexPath:(NSIndexPath *)indexPath  {

    UITableViewCell *cell = nil;
    if ( indexPath.section == 0 ) {
        cell = self.mapCell;

    } else {
        static NSString *identifier = @"cell";
        cell = [tv dequeueReusableCellWithIdentifier:@"cell"];
        if ( cell == nil ) {
            cell = [[UITableViewCell alloc]
                initWithStyle:UITableViewCellStyleDefault
                reuseIdentifier:identifier];
            cell.accessoryType = UITableViewCellAccessoryNone;
        }
        AppDelegate *delegate =
           (AppDelegate *)[[UIApplication sharedApplication] delegate];
        if ( indexPath.section == 1 ) {
            cell.textLabel.text = [delegate.rows objectAtIndex:indexPath.row];
        } else {
            cell.textLabel.text = [NSString stringWithFormat:@"Positions %d",
                [delegate getSizeOfDatabase]];
        }
    }
    return cell;

You’re almost done, but let’s address a subtle user interface issue before deploying your code. If you look at your current interface, the square cornered map inside a round cornered grouped table view cell isn’t going to look good. While having an MKMapView with curved corners is pretty hard to do inside Interface Builder, you can fix it in code fairly easily.

You’re going to use the low level QuartzCore framework to provide the curved corners, so click the project icon at the top of the Project pane in Xcode, click the main Target for the project, and then click the Build Phases tab. Finally, click the Link Binary with Libraries item to open the list of linked frameworks, and click the + symbol to add a new framework. Select the QuartzCore framework (QuartzCore.framework) from the drop-down list and click the Add button to add the framework to your project.

Then click the ViewController.h interface file to open it in the Xcode editor and import the corresponding header files:

#import <QuartzCore/QuartzCore.h>

Save your changes, then open the corresponding implementation file, ViewController.m. In the viewDidLoad: method add the line in bold below.

- (void)viewDidLoad {
    self.mapView.layer.cornerRadius = 10.0;
    [super viewDidLoad];
}

You’re done. Save your changes and click the Run button in the Xcode toolbar to compile and deploy your application into the iPhone Simulator. If all goes well, you should see something that looks a lot like Figure 3-3.

The new user interface

Figure 3-3. The new user interface

This is okay; however, it’s not amazingly exciting. Map Kit knows the user’s location and for this to be displayed on the map, you only need to add the following line to the viewDidLoad: method:

self.mapView.showsUserLocation = YES;

Save the change and click the Build and Run button in the Xcode toolbar again. This time you should see something like Figure 3-4.

Displaying the user’s location on the map

Figure 3-4. Displaying the user’s location on the map

However, while the Map Kit framework is able to mark the user’s location on the map, there is no way to monitor it or update the current map view when the location changes. So you’re going to implement this functionality using the Core Location framework.

Click on the AppDelegate.m implementation file to open it in the editor. Then in the locationManager:didUpdateLocation:fromLocation: delegate method, add the lines in bold below:

- (void)locationManager:(CLLocationManager *)manager
        didUpdateToLocation:(CLLocation *)newLocation
        fromLocation:(CLLocation *)oldLocation {
    NSLog(@"Location: %@", [newLocation description]);

    NSString *latitude = [NSString stringWithFormat:@"Lat. %f degrees", 
                          newLocation.coordinate.latitude];
    NSString *longitude = [NSString stringWithFormat:@"Long. %f degrees", 
                           newLocation.coordinate.longitude];
    NSString *altitude = [NSString stringWithFormat:@"Alt. %f m",
        newLocation.altitude];
    NSString *speed = [NSString stringWithFormat:@"Speed %f m/s",
        newLocation.speed];

    NSString *course = [NSString stringWithFormat:@"Course %f degrees",
        newLocation.course];

    [self.rows insertObject:latitude atIndex:0];
    [self.rows insertObject:longitude atIndex:1];
    [self.rows insertObject:altitude atIndex:2];
    [self.rows insertObject:speed atIndex:3];
    [self.rows insertObject:course atIndex:4];

    [self addLocationToDatabase:newLocation];

    [self.viewController.tableView reloadData];

    double miles = 2.0;
    double scalingFactor =
        ABS( cos(2 * M_PI * newLocation.coordinate.latitude /360.0) );

    MKCoordinateSpan span;
    span.latitudeDelta = miles/69.0;
    span.longitudeDelta = miles/( scalingFactor*69.0 );
    MKCoordinateRegion region;
    region.span = span;
    region.center = newLocation.coordinate;

   [self.viewController.mapView setRegion:region animated:YES];

}

Here you set the map region to be 2 square miles, centered on the current location, and then zoom in and display the current user location.

Note

The number of miles spanned by a degree of longitude range varies based on the current latitude. For example, 1 degree of longitude spans a distance of about 69 miles at the equator but shrinks to 0 at the poles. However, unlike longitudinal distances that vary based on the latitude, 1 degree of latitude is always about 69 miles (ignoring variations due to the slightly ellipsoidal shape of the Earth). The length of 1 degree of Longitude (in miles) equals the cosine of the latitude × 69 miles.

Click the Build and Run button on the Xcode toolbar to compile and deploy the application into the iPhone Simulator. Once the application starts, up you should see something much like Figure 3-5.

Following the user’s location using the Core Location framework

Figure 3-5. Following the user’s location using the Core Location framework

Normally the iPhone will pan and zoom a map when asked to change region, however you’ll probably have noticed that this part didn’t really work that well. This is because you’ve wrapped the map inside a table view cell and asked the table view to reload its data inside the same method where you asked the map to setRegion:animated:. However, you can make use of the scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: method to avoid this problem and make your map pan and zoom properly as the table view is updated inside your Core Location delegate method.

To do so, you’re going to add a new method animateMap:, which you’ll call using an NSTimer event. The method will contain the code you just added to the Core Location delegate, as below:

- (void)locationManager:(CLLocationManager *)manager 
    didUpdateToLocation:(CLLocation *)newLocation 
           fromLocation:(CLLocation *)oldLocation {
    NSLog(@"Location: %@", [newLocation description]);

    NSString *latitude = [NSString stringWithFormat:@"Lat. %f degrees", 
                          newLocation.coordinate.latitude];
    NSString *longitude = [NSString stringWithFormat:@"Long. %f degrees", 
                           newLocation.coordinate.longitude];
    NSString *altitude =
        [NSString stringWithFormat:@"Alt. %f m", newLocation.altitude];
    NSString *speed =
        [NSString stringWithFormat:@"Speed %f m/s", newLocation.speed];

    NSString *course =
        [NSString stringWithFormat:@"Course %f degrees", newLocation.course];

    [self.rows insertObject:latitude atIndex:0];
    [self.rows insertObject:longitude atIndex:1];
    [self.rows insertObject:altitude atIndex:2];
    [self.rows insertObject:speed atIndex:3];
    [self.rows insertObject:course atIndex:4];

    [self addLocationToDatabase:newLocation];

    [self.viewController.tableView reloadData];

    [NSTimer scheduledTimerWithTimeInterval:0.2 target:self
       selector:@selector(animateMap:) userInfo:newLocation repeats:NO];

}

- (void)animateMap:(NSTimer *)timer {

    CLLocation *newLocation = (CLLocation *)[timer userInfo];

    double miles = 2.0;
    double scalingFactor =
       ABS( cos(2 * M_PI * newLocation.coordinate.latitude /360.0) );

    MKCoordinateSpan span;
    span.latitudeDelta = miles/69.0;
    span.longitudeDelta = miles/( scalingFactor*69.0 );

    MKCoordinateRegion region;
    region.span = span;
    region.center = newLocation.coordinate;

    [self.viewController.mapView setRegion:region animated:YES];

}

If you save your changes and click the Run button in the Xcode toolbar again, you should see the map panning and zooming as you’d expect as the table view is updated by the Core Location delegate method.

Annotating Maps

Adding simple map annotations using the Map Kit framework is actually pretty easy. The first thing you need to do is create a class that implements the MKAnnotation delegate protocol. Right-click the Location group in the Project Navigator panel and New File to create a new Objective-C class (make it an NSObject subclass). Name the new class SimpleAnnotation when prompted.

Open the SimpleAnnotation.h interface file Xcode just created in the Standard editor and modify it as follows:

#import <Foundation/Foundation.h>

@interface SimpleAnnotation : NSObject <MKAnnotation>

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

+ (id)annotationWithCoordinate:(CLLocationCoordinate2D)coord;
- (id)initWithCoordinate:(CLLocationCoordinate2D)coord;

@end

Then open the corresponding SimpleAnnotation.m implementation file and make the changes shown:

#import "SimpleAnnotation.h"

@implementation SimpleAnnotation

@synthesize coordinate=_coordinate;
@synthesize title=_title;
@synthesize subtitle=_subtitle;

+ (id)annotationWithCoordinate:(CLLocationCoordinate2D)coord {
   return [[[self class] alloc] initWithCoordinate:coord];
}

- (id)initWithCoordinate:(CLLocationCoordinate2D)coord {
   if ( self = [super init] ) {
      self.coordinate = coord;
   }

   return self;
}

@end

The SimpleAnnotation class is just a container; it implements the MKAnnotation protocol to allow it to hold the coordinates and title (with subtitle) of our annotation.

Click the ViewController.h interface file to open in the Xcode editor, and import the SimpleAnnotation header file:

#import "SimpleAnnotation.h"

Then, in the viewDidLoad: method, add two annotations to our mapView as follows:

    CLLocationCoordinate2D moffett = {37.4163, 122.0519};
    SimpleAnnotation *moffettAnnotation =
      [[SimpleAnnotation alloc] initWithCoordinate:moffett];
    moffettAnnotation.title = @"Moffett Federal Airfield";
    moffettAnnotation.subtitle = @"37.4163, −122.0519";
    [self.mapView addAnnotation: moffettAnnotation];

    CLLocationCoordinate2D sanJose = {37.3647, 121.9338};
    SimpleAnnotation *sanJoseAnnotation =
        [[SimpleAnnotation alloc] initWithCoordinate:sanJose];
    sanJoseAnnotation.title = @"San Jose International";
    sanJoseAnnotation.subtitle = @"37.3647, −121.9338";
    [self.mapView addAnnotation: sanJoseAnnotation];

Save your changes, and change to the AppDelagate.m. You’ll have to expand your view a little bit as your map window currently excludes your new markers. Go into the animateMap: method and change the size of your view from 2 miles to 20 miles.

    double miles = 20.0;

Then click the Run button in the Xcode toolbar to compile and deploy your application in the iPhone Simulator. If all goes well, you should see something much like Figure 3-6.

An annotated map of Cupertino, CA (left) and after tapping on the annotation pin to show the annotation details (right)

Figure 3-6. An annotated map of Cupertino, CA (left) and after tapping on the annotation pin to show the annotation details (right)

Congratulations, you now have an annotated map application that tracks and reports the user’s position, and a good grounding with the Core Location and Map Kit frameworks.

Adding Overlays

Adding polygon overlays to your map is almost as easy as adding annotations. So let’s go back to your Location application and modify it yet again to add a circular overlay to represent the region you’re monitoring. You’ll look at other types of overlays, such as image overlays, when you look at heat maps later on in Chapter 6.

So I can easily illustrate what’s going on in the iPhone Simulator, I’m going to change the latitude and longitude of my geofenced region (which was previously centered on the Exeter Apple Store) to be centered on San Jose International Airport.

To start with, I’m going to open AppDelegate.m in the editor and then in the application:didFinishLaunchingWithOptions: method I’m going to change the latitude, longitude, and radius of my geofence from:

    CLLocationCoordinate2D center = CLLocationCoordinate2DMake(50.72451,-3.52788);
    CLLocationDistance radius = 200.0; // 200m

to:

    CLLocationCoordinate2D center = CLLocationCoordinate2DMake(37.3647,-121.9338);
    CLLocationDistance radius = 3000.0 // 3km

These new coordinates define a geographical region centered on San Jose International Airport, which is 3 km (or 1.9 miles) in radius.

Note

You can keep your own geofenced region as you had it in the last chapter, but remember to change the latitude, longitude, and radius that I’ll use below for the overlay region to be the same as your own geofenced region.

The first thing you need to do now is to create your overlay. You want to do this in the viewDidAppear: method of your view controller. So click the ViewController.m implementation file to open it in the editor and add the following code highlighted below to the method:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    CLLocationCoordinate2D center = CLLocationCoordinate2DMake(37.3647,-121.9338);
    CLLocationDistance radius = 3000.0;
    MKCircle *circle = [MKCircle circleWithCenterCoordinate:center radius:radius];
    [circle setTitle:@" "];
    [self.mapView addOverlay:circle];
}

Here, you’ve defined an MKCircle object and added it to your map view. However, right now your view doesn’t really know what to do with this overlay, or how you want it styled. So, you now need to tell it.

Click the ViewController.h interface file, and modify it as below to declare your view controller as an MKMapViewDelegate:

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

#import "SimpleAnnotation.h"

@interface ViewController : UIViewController <UITableViewDelegate,
    UITableViewDataSource, MKMapViewDelegate>

@property (strong, nonatomic) IBOutlet UITableView *tableView;
@property (strong, nonatomic) IBOutlet UITableViewCell *mapCell;
@property (weak, nonatomic) IBOutlet MKMapView *mapView;

@end

Having done so, click the ViewController.xib NIB file and right-click and drag from the MKMapView inside your UITableViewCell to the File’s Owner icon at the top of the dock to the left of the editor panel (see Figure 3-7). Connect the view controller to the map view as a delegate.

Connecting the map view and view controller

Figure 3-7. Connecting the map view and view controller

Now you need to implement the mapView:viewForOverlay: delegate callback:

- (MKOverlayView *)mapView:(MKMapView *)mapView
        viewForOverlay:(id <MKOverlay>)overlay {

    NSLog(@"Overlaying circle");
    MKCircle *circle = overlay;
    MKCircleView *circleView = [[MKCircleView alloc] initWithCircle:overlay];

    if ([circle.title isEqualToString:@"San Jose Airport"]) {

        circleView.fillColor = [UIColor redColor];
        circleView.alpha = 0.5;
    }
    return circleView;
}

Here, take your MKCircle object, which in reality is an MKOverly object, and generate a view that the map view can use to plot your overlay on top of its map.

Save your changes and click the Run button to build and deploy into the Simulator. You should see something much like Figure 3-8.

The geofenced region is now highlighted with a semi-transparent red circle (left), more clearly seen in the zoomed view (right)

Figure 3-8. The geofenced region is now highlighted with a semi-transparent red circle (left), more clearly seen in the zoomed view (right)

If you’re using a geofenced region of your own, based on your current location, now is the time to build and deploy onto your device and go for a walk and test your code out the old fashioned way.

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

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