Using C# to Develop for iPhone, iPod Touch and iPad

Brian Long Consultancy & Training Services Ltd.
February 2012

Accompanying source files available through this download link

Page selection: Previous  Next

Location/Heading Support With CoreLocation and MapKit

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 OptionCommand6). This is also shown in the screenshot.

GPS page in Interface Builder

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;

You should also clean up in ViewDidDisappear():

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);
        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);
        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);
        altitudeLabel.Text = "N/A";
    if (args.NewLocation.Course >= 0)
        courseLabel.Text = string.Format("{0}°", args.NewLocation.Course);
        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.

GPS functionality in iPhone Simulator

Go back to the top of this page

Go back to start of this article

Previous page

Next page