Brian Long Consultancy & Training Services
Ltd.
March 2011
Accompanying source files available through this
download link
One of the very neat features of the iPhone and other current smartphones is the in-built GPS and compass support. There are many handy applications that can chart your progress during running or cycling, or just record your travelled route, built using this capability.
Basic GPS/compass support is offered through the CoreLocation API and a location-aware
map control is found in the MapKit: the MKMapView
.
Note: the MKMapView
control uses Google’s services to do its work and
by using it you acknowledge that you are bound by their terms, which are
available online.
The GPSPage view in this sample application will use CoreLocation and an
MKMapView
to show the current location, heading, altitude and speed.
To build the UI in Interface Builder you need to lay down 16 labels with text on
as shown in the screenshot below and a Map View. All the labels that say N/A,
as well as the Map View, should be connected to outlets defined in GPSPage
as per the Connections Inspector in the screenshot.
Next we start on the code.
The starting point for location-based functionality is the CLLocationManager
class, so declare a variable locationManager
of this type in your
GPSPage
class (it's in the MonoTouch.CoreLocation namespace). This object
offers us GPS-based information about the location (position, course, speed and
altitude from the GPS hardware - if GPS signal or hardware is not available the
device will provide coarse-grained location information based on cell phone towers
or your WiFi hotspot) and the compass-based heading (the direction the device is
pointing). The GPS-dependant information will be of varying accuracy, as is the
nature of GPS data (you will be locked onto a varying number of satellites).
The location manager offers callback facilities that triggers as the heading and location changes, allowing your journey to be tracked. Depending on the type of application you build you can control how accurate you would like the data to be and you can also control how often your application will be notified of heading and/or location changes. If you weren’t required to track a detailed route, then being notified for every single location change would be excessive. It may be more appropriate to be notified when the location changes by 50 meters, say. Requiring less accuracy and being notified less often is helpful in the context of battery usage.
This callback mechanism is implemented in CoreLocation using the common approach
of supporting a delegate object (inherited from type CLLocationManagerDelegate
),
which has methods to override for location and heading changes. You create an instance
of such a class and assign it to the location manager’s Delegate
property.
An example delegate class might look like the following code (notice that the main
view, GPSPage, is passed into the constructor and is to be stored in the
Page
variable, so it can access controls on the view:
private class CoreLocationManagerDelegate: CLLocationManagerDelegate
{
private GPSPage page;
public CoreLocationManagerDelegate(GPSPage Page)
{
page = Page;
}
public override void UpdatedHeading(CLLocationManager manager, CLHeading newHeading)
{ ... }
public override void UpdatedLocation (CLLocationManager manager, CLLocation newLocation, CLLocation oldLocation)
{ ... }
}
As we have seen before, the MonoTouch approach is to absorb such delegate objects
and their optional methods and expose them as events in the main object. So the
location manager actually has properties called UpdatedHeading
and
UpdatedLocation
. In this code, we’ll use those instead.
The signatures of these methods fit in with the standard .NET event signature:
void UpdatedHeading(object sender, CLHeadingUpdatedEventArgs args);
void UpdatedLocation(object sender, CLLocationUpdatedEventArgs args);
where sender
refers to the location manager and the args
parameters contains properties matching the remaining parameters that are sent to
the matching delegate object method.
In GPSPage.ViewDidAppear()
we’ll initialize the location manager:
locationManager = new CLLocationManager();
locationManager.DesiredAccuracy = -1; //Be as accurate as possible
locationManager.DistanceFilter = 50; //Update when we have moved 50 m
locationManager.HeadingFilter = 1; //Update when heading changes 1 degree
locationManager.UpdatedHeading += UpdatedHeading;
locationManager.UpdatedLocation += UpdatedLocation;
locationManager.StartUpdatingLocation();
locationManager.StartUpdatingHeading();
You should also clean up in ViewDidDisappear()
:
locationManager.StopUpdatingHeading();
locationManager.StopUpdatingLocation();
locationManager.Dispose();
locationManager = null;
Note: the setup/teardown code in this page is done in ViewDidAppear()
and ViewDidDisappear()
(as opposed to ViewDidLoad()
and
ViewDidUnload()
) to avoid the GPS hardware continuing to report information
to the view when you have navigated back to the menu.
We’ll need to look at the event handlers referenced here, but first we should also
initialize the Map View. Above the location manager initialization code in the
ViewDidAppear()
method we need this:
using MonoTouch.MapKit;
...
MapView.WillStartLoadingMap += (s, e) => {
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = true; };
MapView.MapLoaded += (s, e) => {
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = false; };
MapView.LoadingMapFailed += (s, e) => {
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = false; };
MapView.MapType = MKMapType.Hybrid;
MapView.ShowsUserLocation = true;
//Set up the text attributes for the user location annotation callout
MapView.UserLocation.Title = "You are here";
MapView.UserLocation.Subtitle = "YA RLY!";
You can see we have Map View events that mirror the UIWebView
events
and do a similar job (though this time we simply ignore any errors). The MapType
and ShowsUserLocation
properties could actually have been set in Interface
Builder in the Attributes Inspector but instead are set in code. MapType
allows you to make the usual display choice that maps such as Google or Bing offer:
standard (map), satellite, or hybrid (satellite plus road markings). ShowUserLocation
controls whether the map will display the user’s location (using an annotation),
assuming it can be determined. The final property being set, UserLocation
,
customizes this map annotation. When clicked on, the annotation can produce a callout
displaying extra information consisting of a title and subtitle, and that’s what
we are setting here.
Now back to the callback events. The heading change callback is short and simple,
since there are only two new heading values offered. The NewHeading
object inside args
has TrueHeading
(heading relative to
true north) and MagHeading
(heading relative to magnetic north) properties.
It also offers HeadingAccuracy
that indicates how many degrees, one
way or the other, the heading values might be. If this accuracy value is negative,
then heading information could not be acquired, as is the case in the iPhone Simulator.
The Simulator has some GPS functionality, but no emulated compass.
private void UpdatedHeading(object sender, CLHeadingUpdatedEventArgs args)
{
if (args.newHeading.HeadingAccuracy >= 0)
{
MagHeadingLabel.Text = string.Format("{0:F1}° ± {1:F1}°", args.NewHeading.MagneticHeading, args.NewHeading.HeadingAccuracy);
TrueHeadingLabel.Text = string.Format("{0:F1}° ± {1:F1}°", args.NewHeading.TrueHeading, args.NewHeading.HeadingAccuracy);
}
else
{
MagHeadingLabel.Text = "N/A";
TrueHeadingLabel.Text = "N/A";
}
}
The location change callback is a little longer, but only because there are more
values available from the GPS hardware. This time args
has both a
NewLocation
and an OldLocation
CLLocation
object,
so you could work out the distance travelled between the two (CLLocation
offers a DistanceFrom
method) if you chose:
private void UpdatedLocation(object sender, CLLocationUpdatedEventArgs args)
{
const double LatitudeDelta = 0.002;
//no. of degrees to show in the map
const double LongitudeDelta = LatitudeDelta;
var PosAccuracy = args.NewLocation.HorizontalAccuracy;
if (PosAccuracy >= 0)
{
var Coord = args.NewLocation.Coordinate;
//In simulator, MapKit's user location is fixed on Apple's HQ but
//CoreLocation will happily detect current location via network
//(contrary to Apple docs)
LatitudeLabel.Text = string.Format("{0:F6}° ± {1} m", Coord.Latitude, PosAccuracy);
LongitudeLabel.Text = string.Format("{0:F6}° ± {1} m", Coord.Longitude, PosAccuracy);
if (Coord.IsValid())
{
var region = new MKCoordinateRegion(Coord, new MKCoordinateSpan(LatitudeDelta, LongitudeDelta));
MapView.SetRegion(region, false);
MapView.SetCenterCoordinate(Coord, false);
MapView.SelectAnnotation(MapView.UserLocation, false);
}
}
else
{
LatitudeLabel.Text = "N/A";
LongitudeLabel.Text = "N/A";
}
if (args.NewLocation.VerticalAccuracy >= 0)
AltitudeLabel.Text = string.Format("{0:F6} m ± {1} m", args.NewLocation.Altitude, args.NewLocation.VerticalAccuracy);
else
AltitudeLabel.Text = "N/A";
if (args.NewLocation.Course >= 0)
CourseLabel.Text = string.Format("{0}°", args.NewLocation.Course);
else
CourseLabel.Text = "N/A";
SpeedLabel.Text = string.Format("{0} m/s", args.NewLocation.Speed);
}
Breaking the code up, the first big condition deals with the position, updating the latitude and longitude labels with the relevant position and the accuracy achieved, and the Map View position. If the accuracy value is negative then a position has not been obtained and so N/A is written to the labels.
You might notice the comment in the code that talks about the GPS functionality in the Simulator. All references I found, in forums and in the Apple documentation, state that CoreLocation will always return a fixed location in the iPhone Simulator, the location being the Apple HQ at 1 Infinite Loop, Cupertino, CA 95014 with an accuracy of 100m. In my tests this was true of the Map View – if not forced to do otherwise it will always report the user’s location as being at Apple HQ. However CoreLocation would correctly identify my location and return co-ordinates to my office. This seems to contradict various statements and shows some in-Simulator inconsistency between MapKit and CoreLocation.
To keep things consistent the code takes the CoreLocation coordinate as the true location and forces the Map View to use it by specifying a region to display and centering the map on that coordinate (we lose the user location annotation this way, but at least we see where we really are). The map display region is set up in terms of a coordinate and a pair of X and Y deltas, which dictate how much of the earth to display in terms of degrees. A small value has been used for both deltas to show a vaguely recognizable piece of the local territory. This control of the Map View only takes place if the CoreLocation’s coordinate is deemed to be valid. On the first few callbacks it is common for the coordinate to start as invalid while the GPS system gets on top of its communication.
The final thing done with the Map View is a call to SelectAnnotation()
made against the annotation at the user’s location. This is the equivalent of clicking
the annotation and will cause the callout (with the title and subtitle) to be displayed.
Of course, if the app is showing your actual location and the Map View has the user
location annotation in Cupertino, you are unlikely to see it. In the sample code
source (not shown in the listing above) there is a conditional define called SHOW_FAKE_POSITION_IN_SIMULATOR
that you can define to overcome this and ensure the Map View’s notion of the user
location is used for both the information labels and also the map position, and
so showing the user location annotation.
The remaining code performs familiar looking tasks for the altitude and course – displaying the values if they are valid – and also displays the current speed as ascertained by the GPS observations.
The screenshot below shows the GPS Page operating, though it was taken with the aforementioned conditional compilation symbol defined, so the image looks consistent with the Apple documentation. The user location annotation is actually dynamic. As well as the blue marble in the centre and the outer circle indicating the possible inaccuracy radius, the blue circle in between pulses out from the center to the outer circle in a manner pleasing to the eye.
Go back to the top of this page
Go back to start of this article