With the release of iOS 4, the Core Location framework received a significant update by providing support for geofencing. Not only can an application be notified when the device enters or exits a geofence, the operating system also notifies when the application is in the background. This addition to the Core Location framework fits neatly in Apple's endeavor to support multitasking on iOS. This opens up a number of possibilities, such as performing tasks in the background through geofencing and that is exactly what this tutorial is about.
What is Geofencing?
Geofence
A geofence is nothing more than an virtual boundary that corresponds to an actual location in space. In iOS, a geofence is represented by an instance of the CLRegion
class, which is characterized by a coordinate (latitude and longitude), a radius, and a unique identifier. This means that geofences on iOS are by definition circular.
Geofencing
Geofencing is the process in which one or more geofences are monitored and action is taken when a particular geofence is entered or exited. Geofencing is a technology very well suited for mobile devices. A good example is Apple's Reminders application. You can attach a geofence to a reminder so that the application notifies you when you are in the vicinity of a particular location (figure 1). This is incredibly powerful and much more intuitive that using traditional reminders that remind you at a certain date or time.
On the iOS platform, geofencing can be implemented in several ways. The simplest implementation is to use the Core Location framework to monitor one or more geofences. Whenever the user's device enters or exits one of these geofences, the application is notified by the operating system. This also works if the application is not in the foreground, which is crucial if you want to properly implement geofencing in an application.
A more complex implementation of geofencing involves a remote server that tracks the location of the user's device and sends push notifications to the user's device if it enters or exits a geofence. This approach is much more involved and comes with significant overhead. One reason for opting for this strategy is tied to the limitations of the Core Location framework. Let me explain what I mean by that.
Geofencing on iOS
As I told you earlier, the Core Location framework is responsible for geofencing on iOS. Location services are closely tied to multitasking and geofencing is no exception. By enabling geofencing in the background, it appears as if your application continues to run in the background. However, the operating system goes one step further by monitoring regions of interest even if the application is inactive, that is, no instance of the application is running in the background or foreground. The mechanism is not that complicated. The operating system manages a list of geofences and which applications are interested in which geofences. If a particular geofence is entered or exited, the operating system notifies the corresponding application. The primary reason of this approach is to save battery power. Instead of allowing several applications running in the background and using the device's location services, the operating system manages this task and only notifies an application when necessary.
There are a number of downsides to how geofencing works on iOS. For example, an application can monitor no more than twenty regions or geofences. This might be sufficient for most applications, but you can imagine that for some applications this upper limit is unacceptable.
In the remainder of this tutorial, I will show you how to implement geofencing in an iOS application. At the end of the tutorial, I will make some suggestions as to how you can leverage geofencing to perform tasks in the background even if your application is not running.
Step 1: Project Setup
Create a new project in Xcode by selecting the Single View Application template from the list of templates (figure 2). Name your application Geofencing, enter a company identifier, set iPhone for the device family, and check Use Storyboards and Use Automatic Reference Counting. We won't be using unit tests in this project (figure 3). Tell Xcode where you want to save the project and click Create.
Because the Core Location framework will do the heavy lifting for us, we need to link our project against it. Select the project in the Project Navigator and choose the Geofencing target from the list of targets. Choose the Build Phases tab at the top and open the Link Binary With Libraries drawer. Click the button with the plus sign and choose CoreLocation.framework form the list of libraries and frameworks (figure 4).
Step 2: User Interface
Before we create the user interface, we need to make one change to the MTViewController
class. Open the class header file and change the superclass to UITableViewController
.
#import <UIKit/UIKit.h> @interface MTViewController : UITableViewController @end
By using a storyboard, creating the user interface is quick and easy. Open MainStoryboard.storyboard, select the view controller, and delete it from the storyboard. Drag a table view controller from the Object Library on the right and set its class to MTViewController
in the Identity Inspector. With the view controller still selected, open the Editor menu and choose Embed In > Navigation Controller (figure 5).
Select the prototype cell of the table view controller and give it an identifier of GeofenceCell and set the cell's style to Subtitle (figure 6).
The application will need the ability to add and remove geofences. Let's start by creating two buttons. In the view controller's viewDidLoad
method, we call setupView
, a helper method in which we further configure the user interface. In setupView
, we create a button to add a geofence based on the current location and a button to edit the table view.
- (void)viewDidLoad { [super viewDidLoad]; // Setup View [self setupView]; }
- (void)setupView { // Create Add Button self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addCurrentLocation:)]; // Create Edit Button self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Edit", nil) style:UIBarButtonItemStyleBordered target:self action:@selector(editTableView:)]; }
Step 3: Initialization
The CLLocationManager
class is the star player of this tutorial. The location manager will provide us with the coordinates of the device's current location and it will also enable us to work with geofences. Start by adding an import statement to the view controller's header file to import the framework's header files.
#import <UIKit/UIKit.h> #import <CoreLocation/CoreLocation.h> @interface MTViewController : UITableViewController @end
We also need to create two private properties. The first property stores a reference to the location manager, an instance of the CLLocationManager
class. To receive updates from the location manager, the MTViewController
class is required to conform to the CLLocationManagerDelegate
protocol. I will talk more about this in a few moments. The second property is of type NSArray
and serves as the data source of the table view. It will store the geofences, instances of CLRegion
, that the location manager monitors. For practical reasons, I have also declared a helper variable (BOOL
) named _didStartMonitoringRegion
. Its purpose will become clear a bit later in this tutorial.
@interface MTViewController () <CLLocationManagerDelegate> { BOOL _didStartMonitoringRegion; } @property (strong, nonatomic) CLLocationManager *locationManager; @property (strong, nonatomic) NSMutableArray *geofences; @end
In the view controller's awakeFromNib
method, we initialize and configure the location manager as shown below. The awakeFromNib
method is sent to the view controller after each object in the nib file is loaded and ready to use.
- (void)awakeFromNib { [super awakeFromNib]; // Initialize Location Manager self.locationManager = [[CLLocationManager alloc] init]; // Configure Location Manager [self.locationManager setDelegate:self]; [self.locationManager setDesiredAccuracy:kCLLocationAccuracyHundredMeters]; }
Step 4: Populating the Table View
To populate the table view, we need the geofences that the operating system is monitoring for us. The location manager keeps a reference to these geofences, which means that there is no need to store the geofences ourselves. Amend the awakeFromNib
method as shown below. Because the monitoredRegions
property is a set, it is necessary to store a reference of the location manager's monitoredRegions
property in the view controller's geofences
property.
- (void)awakeFromNib { [super awakeFromNib]; // Initialize Location Manager self.locationManager = [[CLLocationManager alloc] init]; // Configure Location Manager [self.locationManager setDelegate:self]; [self.locationManager setDesiredAccuracy:kCLLocationAccuracyHundredMeters]; // Load Geofences self.geofences = [NSMutableArray arrayWithArray:[[self.locationManager monitoredRegions] allObjects]]; }
The implementation of the table view data source protocol holds no surprises (see below). As I mentioned earlier, the geofences
property contains instances of CLRegion
. In each table view cell, we display the location of each region along with the region's unique identifier. We take a closer look at the tableView:commitEditingStyle:forRowAtIndexPath:
method later in this tutorial.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return self.geofences ? 1 : 0; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.geofences count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:GeofenceCellIdentifier]; // Fetch Geofence CLRegion *geofence = [self.geofences objectAtIndex:[indexPath row]]; // Configure Cell CLLocationCoordinate2D center = [geofence center]; NSString *text = [NSString stringWithFormat:@"%.1f | %.1f", center.latitude, center.longitude]; [cell.textLabel setText:text]; [cell.detailTextLabel setText:[geofence identifier]]; return cell; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return YES; } - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { return NO; }
Don't forget to declare the static cell identifier at the top of the view controller's implementation file as shown below.
static NSString *GeofenceCellIdentifier = @"GeofenceCell";
Step 5: Adding a Geofence
To add a geofence, we need to implement the addCurrentLocation:
action. Its implementation is pretty simple as you can see. In the addCurrentLocation:
method, we ask the location manager instance to start generating updates for the device's current location. We also set the helper instance variable _didStartMonitoringRegion
to NO
. I will explain the reason for this in a moment.
- (void)addCurrentLocation:(id)sender { // Update Helper _didStartMonitoringRegion = NO; // Start Updating Location [self.locationManager startUpdatingLocation]; }
Whenever the location manager has location updates available, it invokes the locationManager:didUpdateLocations:
delegate method as shown below. In this delegate method, we first check if _didStartMonitoringRegion
is set to NO
. This is important, because it can happen that location updates are sent so quickly one after the other that multiple geofences are created for one location. We create the region or geofence (CLRegion
) based on the first location in the array of locations gives to us by the location manager. The radius of the region is set to 250.0
(in meters), but feel free to modify this to your needs.
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { if (locations && [locations count] && !_didStartMonitoringRegion) { // Update Helper _didStartMonitoringRegion = YES; // Fetch Current Location CLLocation *location = [locations objectAtIndex:0]; // Initialize Region to Monitor CLRegion *region = [[CLRegion alloc] initCircularRegionWithCenter:[location coordinate] radius:250.0 identifier:[[NSUUID UUID] UUIDString]]; // Start Monitoring Region [self.locationManager startMonitoringForRegion:region]; [self.locationManager stopUpdatingLocation]; // Update Table View [self.geofences addObject:region]; [self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:([self.geofences count] - 1) inSection:0]] withRowAnimation:UITableViewRowAnimationLeft]; // Update View [self updateView]; } }
The identifier of the CLRegion
instance ensures that we can distinguish one monitored region from another. If we were to monitor a new region with the same identifier, the old region would be replaced with the new region. To start monitoring the new region, we send the location manager a message of startMonitoringForRegion:
and pass the new region as an argument. With the new region being monitored, we also stop updating the current location.
To update the table view, we first update its data source and then insert a row at the bottom of the table view. The view is also updated by calling another helper method, updateView
. This method performs several checks and updates the view controller's view accordingly.
- (void)updateView { if (![self.geofences count]) { // Update Table View [self.tableView setEditing:NO animated:YES]; // Update Edit Button [self.navigationItem.rightBarButtonItem setEnabled:NO]; [self.navigationItem.rightBarButtonItem setTitle:NSLocalizedString(@"Edit", nil)]; } else { // Update Edit Button [self.navigationItem.rightBarButtonItem setEnabled:YES]; } // Update Add Button if ([self.geofences count] < 20) { [self.navigationItem.leftBarButtonItem setEnabled:YES]; } else { [self.navigationItem.leftBarButtonItem setEnabled:NO]; } }
Whenever the user's device enters and exits a monitored region, the location manager's delegate is sent a message of locationManager:didEnterRegion:
and locationManager:didExitRegion:
, respectively. Implement both delegate methods as shown below.
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region { NSLog(@"%s", __PRETTY_FUNCTION__); } - (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region { NSLog(@"%s", __PRETTY_FUNCTION__); }
Before running the application in the iOS Simulator, we need to modify the active scheme. Click Geofencing on the immediate right of the Stop button (figure 7) and select Edit Scheme... from the menu. On the left, select the tab labeled Run Geofencing.app and choose the Options tab at the top (figure 8). Check the checkbox labeled Allow Location Simulation and set the default location to New York, NY, USA (figure 8). This will make it easier to test our application.
Build and run the application in the iOS Simulator and add a region to monitor by clicking the add button in the top left. You can test the geofence by changing the simulated location of the iOS Simulator (figure 9).
Step 6: Editing Geofences
Editing the geofences is quite easy. Start by implementing the editTableView:
action as shown below. All we do is toggle the editing mode of the table view and update the title of the edit button.
- (void)editTableView:(id)sender { // Update Table View [self.tableView setEditing:![self.tableView isEditing] animated:YES]; // Update Edit Button if ([self.tableView isEditing]) { [self.navigationItem.rightBarButtonItem setTitle:NSLocalizedString(@"Done", nil)]; } else { [self.navigationItem.rightBarButtonItem setTitle:NSLocalizedString(@"Edit", nil)]; } }
To remove a geofence from the table view (and stop monitoring the region), we implement the tableView:commitEditingStyle:forRowAtIndexPath:
as shown below. Its implementation is not complicated as you can see. We fetch the corresponding region from the data source, tell the location manager to stop monitoring that region, update the table view and its data source, and update the view. Build and run the application once more to try it out.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // Fetch Monitored Region CLRegion *region = [self.geofences objectAtIndex:[indexPath row]]; // Stop Monitoring Region [self.locationManager stopMonitoringForRegion:region]; // Update Table View [self.geofences removeObject:region]; [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight]; // Update View [self updateView]; } }
A Bit of Hacking
Developers have tried to bypass the limitations of the iOS operating system for many years. One of the hacks that some applications use to update an application in the background is geofencing. This is an obvious strategy for applications, such as Foursquare or TomTom. However, Apple seems to allow other applications to use geofencing as well. Instapaper, for example, implemented this strategy some time ago. By defining one or more geofences, Instapaper starts updating your list of saved articles in the background whenever you enter one of those geofences. This is a clever strategy than can be useful in a number of use cases.
Status Bar
Whenever an application is actively using the device's location services, the location services icon is shown in the status bar. The same is true for geofencing. The difference is the icon as shown in figure 10. Even if no application is running in the foreground, the outlined location services icon is visible as long as any application on the device is making use of geofencing.
Things To Keep in Mind
I have only covered the absolute fundamentals of geofencing in this tutorial. It is important to keep a few things in mind before implementing geofencing in any application. The most obvious aspect is privacy. A user can disable location services for any application, which means that geofencing is not guaranteed to work in every use case. It is important to perform three checks when working with Location Services, (1) are location services enabled, (2) has the user given your application permission to use the device's location services, and (3) does the device support geofencing.
The first question is answered by sending the CLLocationManager
class a message of locationServicesEnabled
. This tells the application whether the device has location services enabled. To verify if your application is allowed to make use of the device's location services, you send the CLLocationManager
a message of locationServicesEnabled
. Finally, to check whether geofencing is available on the device, invoke CLLocationManager
's class method regionMonitoringAvailable
. Not all devices and models support geofencing.
Conclusion
Implementing geofencing in an iOS application isn't that difficult as you can see. A basic understanding of the Core Location framework is all you need to get started. The Core Location framework is a powerful framework and it has a lot more neat features that are often overlooked or underused.
Comments