Brian Long Consultancy & Training Services
Ltd.
February 2012
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 GPSViewController screen 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
as per the screenshot.
Note: you can also see all the defined
outlets by selecting the File's Owner in the Dock and looking at the Connections
Inspector (choose View, Utilities, Show Connections Inspector or press
6
).
This is also shown in the screenshot.
Note: you may get parse errors or build errors shown in Xcode thanks to the use
of the MKMapView
, but these can be ignored.
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
GPSViewController
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,
GPSViewController
, 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 GPSViewController page;
public CoreLocationManagerDelegate(GPSViewController 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 GPSViewController.ViewDidAppear()
(you'll need to
add in an override for this method) 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;
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.
Note: the iPhone simulator can simulate location data via its Debug, Location menu. You can choose a custom location, specified as a coordinate pair, choose the location of Apple HQ (at 1 Infinite Loop, Cupertino, CA 95014), or choose options that simulate a route.
The code identifies the user location (from the CoreLocation coordinate) and, to ensure this stays on-screen, forces the Map View to use it by specifying a region to display and centering the map on that coordinate. 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.
The remaining code performs familiar looking tasks for the altitude and course – displaying the values if they appear valid – and also displays the current speed as ascertained by the GPS observations.
The screenshot below shows the GPS Page operating. 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