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