15 Comments

Tall Trucks, Low Bridges, and iOS Geofences

As my colleague Jeanette and I were walking home from the office a few months back, we came upon an unpleasant-looking accident approaching the bridge west of 1st Street on Washington. This unfortunate truck was too tall for this bridge’s 10’6″ clearance, and as such incurred serious damage.

An unfortunate truck

The experience made me wonder: could I create a mobile application that could warn me (in the unlikely event I was driving a truck taller than ten feet) that I was approaching this bridge, giving me time to stop and seek an alternate route? It turned out that iOS’ geofence support was just the ticket.

What Is Geofencing?

A geofence is an invisible physical region on the surface of the Earth. Notifications are sent when a location-capable device physically enters or exits that region.

Geofences in iOS are circular regions with a latitude and longitude at the center and a radius. To register our intent to be notified when an iOS device enters or exits one or more geofences, we use the CoreLocation framework, asking it to start monitoring for the geofence regions we’ve provided.

Geofence

We use geofences (instead of simply asking the device to deliver constant location updates) for two reasons:

  • Efficiency – It doesn’t take nearly as much power to let iOS do its own geofence calculations.
  • Privacy – We don’t need to know everywhere the user goes just so that we can figure out that they crossed over into our interesting area.

Asking Permission, Not Forgiveness

Before we can use any kind of location service on iOS, we need permission from the user, of course. Before iOS 8, apps could just try to use location services, and iOS would prompt the user on the app’s behalf for wide-ranging location permission. Apple has improved the permissions scheme since, splitting location permission into “always” and “when in use” variations—and now requires us to explicitly ask for one or the other.

For geofences, we need the “always” type of location permission. This doesn’t mean that we’ll be tracking the user’s precise location everywhere, though we will be always notified when they enter or leave geofences.

To get this permission, we need to first explain to the user what we’re going to do with the permission by setting a custom iOS target property. Select your project in Xcode, then the app target, Info, and add a property for NSLocationAlwaysUsageDescription:

Screen Shot 2015-06-02 at 5.52.33 PM

The other thing we should do before we ask for permission is check what our existing location authorization status is, by calling authorizationStatus on CLLocationManager. If it’s undetermined or (somehow) the “when in use” variety, we can go ahead and ask for the “always” permission.

If we already have our “always” permission, our request will just be a no-op; but if location authorization has been restricted or denied, it won’t do us any good to ask. The most we can do is advise the user that we’re not going to be able to provide location services unless they turn them back on in the Settings app.


switch ([CLLocationManager authorizationStatus]) {
    case kCLAuthorizationStatusNotDetermined:
    case kCLAuthorizationStatusAuthorizedWhenInUse:
        [self askForPermission];
        return;
 
    case kCLAuthorizationStatusAuthorizedAlways:
        [self setUpGeofences];
        return;
 
    case kCLAuthorizationStatusDenied:
    case kCLAuthorizationStatusRestricted:
        [self showSorryAlert];
        return;
}

Now, we can finally ask for permission. To do this, we’ll need to instantiate (and keep a strong reference to) a CLLocationManager and use requestAlwaysAuthorization, wrapped in a respondsToSelector check so we don’t crash pre-iOS-8 devices doing this. If we’re on a pre-iOS-8 device, we can go ahead and move on to setting up geofences—these versions of iOS will ask permission for us when we try.


self.locationManager = [CLLocationManager new];
if ([self.locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) {
    [self.locationManager requestAlwaysAuthorization];
} else {
    [self setUpGeofences];
}

The response to the permission grant is asynchronous, so you’ll need to implement the CLLocationManagerDelegate protocol and implement didChangeAuthorizationStatus to detect if permission was granted, or the user denied you outright. The new status is of the same CLAuthorizationStatus type that authorizationStatus gave before. Once you have the “always” permission, you’re ready to move on.

Erecting the Geofences

Our next step is to register the geofences we’re interested in with our CLLocationManager. Geofences are defined as instances of CLRegion, though the only ones that actually work are CLCircularRegion. To create one, you’ll need a latitude, longitude, and radius in meters, as well as a string identifier for your region:


CLLocationCoordinate2D center = CLLocationCoordinate2DMake(42.280597,
                                                          -83.751891);
CLRegion *bridge = [[CLCircularRegion alloc]initWithCenter:center
                                                    radius:100.0
                                                identifier:@"Bridge"];

I’ve picked 100 meters for this geofence for a few reasons. It’s roughly the size of a block, for starters, giving you time to stop and not go under the bridge (though 200 meters might be better, giving you a chance to route around the hazard!) It’s also, in my testing with several iOS devices, proven to be a good, reliable, minimum geofence size.

Once you’ve created your region, you can ask iOS to start monitoring for it with startMonitoringForRegion. It’s safe to call this at any time—if you call it with a region whose identifier already exists, the old region will be replaced.


[self.locationManager startMonitoringForRegion:bridge];

Be sure you implement the CLLocationManagerDelegate methods for both success (didStartMonitoringForRegion) and failure (monitoringDidFailForRegion) so that your app is aware of the status of your monitoring request. There are several reasons geofence monitoring can fail, so it pays to listen to what iOS is telling you.

Entries and Exits

Once iOS is monitoring for your geofences, implement the appropriate CLLocationManager methods for geofence entry (didEnterRegion) and exit (didExitRegion) and iOS will call them when at the appropriate times—even launching your app in the background if it isn’t already running. For my app, I chose to deliver local notifications (which, conveniently, appear on my smartwatch as well.)

One caveat about monitoring for geofence entry: if you need to know whether the device is already in the geofence when you start monitoring, you’ll need to ask separately—you won’t receive a didEnterRegion call unless the device actually moves from outside a geofence to inside. I’ve tried several solutions for this, and the best one I’ve come up with is to call startUpdatingLocation, use CLCircularRegion‘s containsCoordinate to test whether the latitude and longitude are inside the region, and finally call stopUpdatingLocation.

Putting it all together, the answer seems to be yes—in the event I’m ever driving a truck over ten feet tall, I can use my phone to give me another shot at avoiding disaster.

Watch out for that bridge!