Mono

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

Brian Long Consultancy & Training Services Ltd.
March 2011

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

GPS page in Interface Builder

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.

GPS functionality in iPhone Simulator

Go back to the top of this page

Go back to start of this article

Previous page

Next page