Sign up for a Parse account to implement this tutorial and more!

Sign Up

Icon_geolocation_ios
Geolocations

This tutorial teaches you how to create a simple geolocation-enabled application using PFGeoPoint and PFQueryTableViewController.

geopoint
PFGeoPoint
PFQueryTableViewController
storyboard

Download code for this tutorial:

.zip File GitHub

Geolocations is a sample application that demonstrates how you can use Parse to display a list of items, store location data, and query for objects near a location. The source code for this application is available on GitHub. It is recommended that you download and open this project in Xcode to use as reference while reading this tutorial.

1. Geolocations Overview
This section will cover the general architecture of the Geolocations sample app.

2. Displaying a List of Locations
Learn how to display a list of objects that are stored on Parse.

3. Displaying the Location on a Map
Use iOS's MapKit to display geolocation information.

4. Geo Queries
Learn how Geo Queries can help you obtain objects near a location.

1. Geolocations Overview

The Geolocations sample app will show a list of all the locations where the user has been. To tag a location, the user taps the + button. Tapping on a row will present the location as a pin on a map. The user may also look up other locations by tapping on the Search button, which will display annotations near the user's current location.

1.1 Architecture

Geolocations consists of three view controllers. The MasterViewController is the initial screen that shows a list of locations. It is a PFQueryTableViewController sub-class, and its configuration is covered in Section 2.

When the user taps a row in the MasterViewController table view, the DetailViewController will be presented and the location will be visualized in a map view. This map view uses GeoPointAnnotation to represent the location's red pin. Go on to Section 3 to learn more about maps and annotations.

The main view also has a loupe button that brings up our SearchViewController when tapped. The SearchViewController also contains a map, but this time it shows a purple pin with a circular overlay which represents a Geo Query, which is covered in Section 4. Any locations previously saved that fall under this overlay will be shown as red pins. The purple pin is represented by the GeoQueryAnnotation and the overlay is a CircleOverlay.

1.2 Storyboard

All of these view controllers and their interactions are initially set up in a Storyboard called MainStoryboard. The root view controller is a navigation controller with MasterViewController as its own root view controller. The MasterViewController can present two segues, one for DetailViewController and a second for SearchViewController.

2. Displaying a List of Locations

The main screen of the Geolocations app will display a list of all the locations saved by the user. This screen will also have a button that records the user's current location when tapped. Let's start with the list of locations.

2.1. PFQueryTableViewController

A UITableViewController is commonly used to display a list of items on iOS apps. Since each of our locations will be stored as a PFObject of class "Location", we can use PFQueryTableViewController. This built-in UI provided by Parse is great for displaying a single-section table view where each row is backed by a single PFObject.

To get started using PFQueryTableViewController, first set it up with the class of PFObject it will be presenting. Geolocations will use it in MasterViewController, which has been set up as a sub-class of PFQueryTableViewController:

@interface MasterViewController : PFQueryTableViewController <CLLocationManagerDelegate>

When setting up your own custom PFQueryTableViewController sub-classes, you can either use the template file as a starting point, or simply set it up in the Identify Inspector when using Storyboards.

For Geolocations, we opted for the latter. If you click on the Master View Controller - Geolocations Scene in MainStoryboard.storyboard and bring up the Identity Inspector (in Xcode, select View -> Utilities -> Show Identity Inspector), you will notice that this Scene is backed by MasterViewController, which is our sub-class of PFQueryTableViewController. You can use the Identity Inspector to set up your PFQueryTableViewController.

First we need to set up the PFObject class that will be displayed by this view. In our case, this will be "Location". We will also enable pull-to-refresh and pagination with 25 objects per page. You can configure all these settings by clicking on the + under User Defined Runtime Attributes and typing in the following settings:

className (String) = Location; objectsPerPage (Number) = 25; paginationEnabled (Boolean) = YES; pullToRefreshEnabled (Boolean) = YES

If you're using PFQueryTableViewController in your project and not using Storyboards, you can set up these properties in initWithStyle::

- (id)initWithStyle:(UITableViewStyle)style {
    self = [super initWithStyle:style];
    if (self) {
        self.className = @"Location";
        self.objectsPerPage = 25;
        self.paginationEnabled = YES;
        self.pullToRefreshEnabled = YES;
    }
    return self;
}

Your PFQueryTableViewController is now set up to display "Location" PFObjects. So how do we create these? First we need to know where the user is located.

2.2. Using Core Location to Obtain the User's Current Location

To use Core Location in your project, the CoreLocation.framework needs to be added under Build Phases. You will also need to #include <CoreLocation/CoreLocation.h> from any files that will use the Core Location APIs.

We will be using Core Location in the MasterViewController class, which adopts the CLLocationManagerDelegate protocol. It sets up a CLLocationManager object with itself as the delegate in the locationManager method:

- (CLLocationManager *)locationManager {
    if (_locationManager != nil) {
        return _locationManager;
    }

    _locationManager = [[CLLocationManager alloc] init];
    [_locationManager setDesiredAccuracy:kCLLocationAccuracyNearestTenMeters];
    [_locationManager setDelegate:self];
    [_locationManager setPurpose:@"Your current location is used to demonstrate PFGeoPoint and Geo Queries."];

    return _locationManager;
}

We start updating the user's location on viewDidLoad:

[[self locationManager] startUpdatingLocation];

Once a location is obtained, the locationManager:didUpdateToLocation:fromLocation: method on MasterViewController will be called. We will use this method to enable the + and loupe buttons, but we will not read the user's location at this moment. Instead, we will read the location property on our CLLocationManager once the user taps the + button.

CLLocation *location = _locationManager.location;

You can read more about making your iOS application location-aware on Apple's Location Awareness Programming Guide.

2.3 Saving the Location on Parse

The insertCurrentLocation: method on MasterViewController has been linked to the + button in MainStoryboard. Whenever the user taps that button, we will create a PFGeoPoint based on their current location:

CLLocation *location = _locationManager.location;
CLLocationCoordinate2D coordinate = [location coordinate];
PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLatitude:coordinate.latitude 
                                              longitude:coordinate.longitude];

Once we have a PFGeoPoint, we can create a new "Location" PFObject and add the geopoint to the "location" field. Finally, save the PFObject, using the completion block to trigger a refresh of our PFQueryTableViewController:

PFObject *object = [PFObject objectWithClassName:@"Location"];
[object setObject:geoPoint forKey:@"location"];
[object saveEventually:^(BOOL succeeded, NSError *error) {
  if (succeeded) {
    // Reload the PFQueryTableViewController
    [self loadObjects];
  }
}];

Try it! Each time you tap on the + button, a new row with your current location will be added to the list.

3. Displaying the Location on a Map

Now that you have a list of locations in your app, it's time to display these in a map. You can use MKMapView, which is part of the MapKit Framework on iOS. The location can be represented with a red pin. Let's add an annotation to our map.

3.1. Setting Up the Annotation

We will use a GeoPointAnnotation class which implements the MKAnnotation protocol. This protocol requires a CLLocationCoordinate2D coordinate property, and optionally NSString title and subtitle properties for the annotation's callout view.

Our designated initializer, initWithObject:, will take a PFObject as a parameter, which can be used to set up the coordinate and the titles for this annotation in the setGeoPoint: method.

- (void)setGeoPoint:(PFGeoPoint *)geoPoint {
    _coordinate = CLLocationCoordinate2DMake(geoPoint.latitude, geoPoint.longitude);
    
    static NSDateFormatter *dateFormatter = nil;
    if (dateFormatter == nil) {
        dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setTimeStyle:NSDateFormatterMediumStyle];
        [dateFormatter setDateStyle:NSDateFormatterMediumStyle];
    }
    
    static NSNumberFormatter *numberFormatter = nil;
    if (numberFormatter == nil) {
        numberFormatter = [[NSNumberFormatter alloc] init];
        [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
        [numberFormatter setMaximumFractionDigits:3];
    }
    
    _title = [dateFormatter stringFromDate:[self.object updatedAt]];
    _subtitle = [NSString stringWithFormat:@"%@, %@", [numberFormatter stringFromNumber:[NSNumber numberWithDouble:geoPoint.latitude]],
                 [numberFormatter stringFromNumber:[NSNumber numberWithDouble:geoPoint.longitude]]];    
}

Since this annotation can be dragged and dropped to another point in the map, it needs to be able to update its PFGeoPoint when this happens.

- (void)setCoordinate:(CLLocationCoordinate2D)newCoordinate {
  PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLatitude:newCoordinate.latitude longitude:newCoordinate.longitude];
  [self setGeoPoint:geoPoint];
  [self.object setObject:geoPoint forKey:@"location"];
  [self.object saveEventually:^(BOOL succeeded, NSError *error) {
    if (succeeded) {
      [[NSNotificationCenter defaultCenter] postNotificationName:@"geoPointAnnotiationUpdated" object:self.object];
    }
  }];
}

We use NSNotificationCenter to post a notification that will be picked up by MasterViewController, triggering a refresh of the main table view whenever the coordinate for a record changes.

3.2. Setting Up the Map

The map view will be handled by DetailViewController, which has been set up with a MKMapView in MainStoryboard. When a cell is tapped in MasterViewController, the showDetail segue will set up the selected row's PFObject on the DetailViewController being presented:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  if ([[segue identifier] isEqualToString:@"showDetail"]) {
      // Row selection
      NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
      PFObject *object = [self.objects objectAtIndex:indexPath.row];
      [[segue destinationViewController] setDetailItem:object];
  } // ...

Now that our DetailViewController knows which object it will be representing, we need to center the map around this location and show the annotation. We can set it up in DetailViewController's viewDidLoad method. We first obtain the geopoint from the PFObject:

PFGeoPoint *geoPoint = [self.detailItem objectForKey:@"location"];

Second, center the map view around this geopoint:

[self.mapView setRegion:MKCoordinateRegionMake(
  CLLocationCoordinate2DMake(geoPoint.latitude, geoPoint.longitude), 
  MKCoordinateSpanMake(0.01, 0.01)
  )];

Finally, create a GeoPointAnnotation and add it to the map view:

GeoPointAnnotation *annotation = [[GeoPointAnnotation alloc] initWithObject:self.detailItem];
[self.mapView addAnnotation:annotation];

3.3. Displaying Annotations

We still need to handle the actual display of this annotation. This is achieved using the MKMapViewDelegate method mapView:viewForAnnotation:. In it, we will dequeue a reusable annotation, and if there's none available, create one:

annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation 
                                                 reuseIdentifier:GeoPointAnnotationIdentifier];
annotationView.pinColor = MKPinAnnotationColorRed;
annotationView.canShowCallout = YES;
annotationView.draggable = YES;
annotationView.animatesDrop = YES;

This sets up a red pin that can show a callout and be dragged around the map.

We now have a list of locations that have been saved by the user. They are listed in reverse chronological order in the main screen, and you may quickly visualize it in a world map by tapping on the location. Next, we will use Geo Queries to find locations within a given distance around a coordinate.

4. Geo Queries

Parse provides various constraints that can be used to query for objects in a given geographical location. You can query for objects near a PFGeoPoint, or query for objects that are located in a geographical bounding box.

Let's set up a map view where the user can drag and drop a pin which in turn displays any locations within a given distance.

4.1. Setting Up the Annotation and Overlay

We've already covered how annotations are set up and added to a map. We will be using a second type of annotation for our purple pin, GeoQueryAnnotation, which will represent the purple pin in our search view. It's very similar to GeoPointAnnotation, but it adds a CLLocationDistance radius property, and does not use a PFObject as its data source. We will use the radius property in the next section when setting up the circle that will be displayed around our annotation.

We can use MapKit overlays to display a circle around our annotation. This is set up in the CircleOverlay class, which implements the MKOverlay protocol. In addition to a coordinate property, this protocol requires a MKMapRect boundingMapRect property which encompasses the overlay.

- (MKMapRect)boundingMapRect {
    MKMapPoint centerMapPoint = MKMapPointForCoordinate(_coordinate);
    MKCoordinateRegion region =
    MKCoordinateRegionMakeWithDistance(_coordinate, _radius * 2, _radius * 2);
    return MKMapRectMake(centerMapPoint.x,
                         centerMapPoint.y,
                         region.span.latitudeDelta,
                         region.span.longitudeDelta);
}

4.2. Configuring the Overlay

The search view is handled in SearchViewController. The purple pin and overlay will be initially placed in the user's current location, but it may be dragged around the map and its radius adjusted. Let's add all the annotation and overlay configuration code to a configureOverlay method, which we will call whenever the annotation or overlay needs to be adjusted.

First, we remove any annotations or overlays from our map view. Then we create and add the pin annotation and circle overlay to our map.

[self.mapView removeAnnotations:[self.mapView annotations]];    
[self.mapView removeOverlays:[self.mapView overlays]];

CircleOverlay *overlay = [[CircleOverlay alloc] initWithCoordinate:self.location.coordinate radius:self.radius];
[self.mapView addOverlay:overlay];

GeoQueryAnnotation *annotation = [[GeoQueryAnnotation alloc] initWithCoordinate:self.location.coordinate radius:self.radius];
[self.mapView addAnnotation:annotation];

[self updateLocations];

Whenever the overlay is added or moved, we need to update the red location pins around it. We will talk about updateLocations in a moment.

4.3. Displaying the Overlay

Overlays have their own MKMapViewDelegate function for display, mapView:viewForOverlay:. We want to a purple overlay that represents the radius of the Geo Query. We also want to display a red overlay that represents the radius of the target Geo Query as the slider is adjusted.

We can set up both overlays by checking if the overlay being displayed by this call to mapView:viewForOverlay: is the red targetOverlay.

if (overlay == self.targetOverlay) {
    annotationView.fillColor = [UIColor colorWithRed:1.0f green:0.0f blue:0.0f alpha:0.3f];
    annotationView.strokeColor = [UIColor redColor];
    annotationView.lineWidth = 1.0f;
} else {
    annotationView.fillColor = [UIColor colorWithWhite:0.3f alpha:0.3f];
    annotationView.strokeColor = [UIColor purpleColor];
    annotationView.lineWidth = 2.0f;
}

4.4 Displaying the Annotations

Our search map can display two types of annotations. The main one will be our purple annotation, which marks the location around which we will be querying.

if ([annotation isKindOfClass:[GeoQueryAnnotation class]]) {
    MKPinAnnotationView *annotationView =
    (MKPinAnnotationView *)[mapView
                            dequeueReusableAnnotationViewWithIdentifier:GeoQueryAnnotationIdentifier];
    
    if (!annotationView) {
        annotationView = [[MKPinAnnotationView alloc]
                          initWithAnnotation:annotation
                          reuseIdentifier:GeoQueryAnnotationIdentifier];
        annotationView.tag = PinAnnotationTypeTagGeoQuery;
        annotationView.canShowCallout = YES;
        annotationView.pinColor = MKPinAnnotationColorPurple;
        annotationView.animatesDrop = NO;
        annotationView.draggable = YES;
    }
    
    return annotationView;
} 
// ...

The map view will also need to display red annotations for each location returned by the Geo Query.

// ...
else if ([annotation isKindOfClass:[GeoPointAnnotation class]]) {
    MKPinAnnotationView *annotationView =
    (MKPinAnnotationView *)[mapView
                            dequeueReusableAnnotationViewWithIdentifier:GeoPointAnnotationIdentifier];
    
    if (!annotationView) {
        annotationView = [[MKPinAnnotationView alloc]
                          initWithAnnotation:annotation
                          reuseIdentifier:GeoPointAnnotationIdentifier];
        annotationView.tag = PinAnnotationTypeTagGeoPoint;
        annotationView.canShowCallout = YES;
        annotationView.pinColor = MKPinAnnotationColorRed;
        annotationView.animatesDrop = YES;
        annotationView.draggable = NO;
    }
    
    return annotationView;
}

4.5 Adjusting our Geo Query Radius

The radius is controlled by a UISlider in the SearchViewController. When the user drags this slider, we show a red target overlay. This overlay is updated whenever the slider's value is changed.

- (IBAction)sliderValueChanged:(UISlider *)aSlider {
    self.radius = aSlider.value;
    
    if (self.targetOverlay) {
        [self.mapView removeOverlay:self.targetOverlay];
    }

    self.targetOverlay = [[CircleOverlay alloc] 
                            initWithCoordinate:self.location.coordinate
                                        radius:self.radius];
    [self.mapView addOverlay:self.targetOverlay];
}

When the user lets go of the slider, we refresh our original purple overlay.

- (IBAction)sliderDidTouchUp:(UISlider *)aSlider {
    if (self.targetOverlay) {
        [self.mapView removeOverlay:self.targetOverlay];
    }

    [self configureOverlay];
}

4.6 Fetching Locations Using Geo Queries

We've set up a purple pin that can be dragged around the map, and we've set up a slider that can adjust the radius of the overlay around this pin. We will craft the Geo Query in the updateLocations method mentioned earlier.

This query will use the location constraint nearGeoPoint:withinKilometers: on PFQuery, and will add each location object as an annotation to the map view.

- (void)updateLocations {
    CGFloat kilometers = self.radius/1000.0f;

    PFQuery *query = [PFQuery queryWithClassName:@"Location"];
    [query setLimit:1000];
    [query whereKey:@"location"
       nearGeoPoint:[PFGeoPoint geoPointWithLatitude:self.location.coordinate.latitude
                                           longitude:self.location.coordinate.longitude]
   withinKilometers:kilometers];
    [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
        if (!error) {
            for (PFObject *object in objects) {
                GeoPointAnnotation *geoPointAnnotation = [[GeoPointAnnotation alloc]
                                                          initWithObject:object];
                [self.mapView addAnnotation:geoPointAnnotation];
            }
        }
    }];
}