Beacon Ranging in the Background

When beacon ranging is active on your iPhone, apps can subscribe to regular updates of the beacons in range. I’ll show you how to set up beacon ranging such that apps receive these updates both in the foreground (easy) and in the background (not so easy).

Introduction

When beacon ranging is active, CLLocationManager sends regular updates about the beacons in range of your iPhone to CLLocationManagerDelegate. CLLocationManager calls the function

locationManager(manager: CLLocationManager, 
                didRangeBeacons beacons: [CLBeacon], 
                inRegion region: CLBeaconRegion)

of its delegate (see Apple’s documentation for details). The parameter beacons holds the beacons that are currently in range of your iPhone and that are contained in the given region.

You will reimplement this function in the class derived from CLLocationManagerDelegate. Based on the beacons in range, the reimplemented function determines the location of the iPhone in a museum, airport or retail store. I’ll decribe how to locate an exhibit in a room in a future post. Here in this post, I’ll “only” describe how to configure CLLocationManager such that the function above is called no matter whether the app is running in the foreground or background.

Configuring CLLocationManager consists of four steps:

  • Setting the prompt for authorising location in the app’s settings (Info.plist).
  • Switching on the background mode for location updates in the app’s settings (Info.plist).
  • Initialising CLLocationManager to perform beacon ranging.
  • Starting and stopping beacon ranging dynamically.

Step 1: Prompt for Authorising Location Updates

When an app uses location like any app using beacon ranging does, it must ask the user to authorise the use of the iPhone’s location services because of privacy reasons. The app can request to have access to the location services “always”, when the app is in use, or “never”. When-in-use authorisation is enough for beacon ranging.

When the app requests this authorisation, it shows a dialog that can be confirmed or rejected by the user. You can customise the message shown in this dialog by assigning a text of your choice – “Required for proper indoor location”, example – to the settings key NSLocationWhenInUseUsageDescription in Info.plist file of your project.

prompt-when-in-use

Step 2: Background Mode for Location Updates

If the app shall receive location updates while in the background, you must enable “Location updates” in the app’s settings. You can simply tick “Location updates” in Xcode’s project settings “<project> | Capabilities | Background Modes”, where you replace “<project>” by the name of your project.

Note that I have ticked the box for “Audio, AirPlay and Picture in Picture”, because my sample project – an audio guide for museums using beacons – plays audio in the background as well. If you need not play audio in the background, you leave this box unticked.

background-mode-location

Step 3: Initialising Beacon Ranging

When the app starts, CLLocationManager must be initialised for beacon ranging in the foreground and background. The following code snippet shows the essential parts.

import CoreLocation

class AGLocationManager: NSObject, CLLocationManagerDelegate {
    var locationManager: CLLocationManager
    let proximityID: NSUUID

    override init() {
        self.locationManager = CLLocationManager()
        self.proximityID = NSUUID(UUIDString: "F29DC74A-DBBD-12DE-318A-C8B046DD4425")!
        super.init()        
        self.initLocating()
    }

    func initLocating() {
        if CLLocationManager.isRangingAvailable() {
            self.locationManager.requestWhenInUseAuthorization()
            self.locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
            self.locationManager.allowsBackgroundLocationUpdates = true
            self.locationManager.delegate = self
        }
    }

    func locationManager(manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], inRegion region: CLBeaconRegion) {
        // Determining the location of the iPhone's user...
    }
}

In my sample app, AGLocationManager is the class responsible for determining the user’s location. It inherits the protocol CLLocationManagerDelegate to receive the location updates through the function locationManager(_:didRangeBeacons:inRegion:).

AGLocationManager holds an instance of CLLocationManager from the CoreLocation framework in its member variable locationManager. Its constant member variable proximityID stores the UUID of the beacon region that your app is interested in. Your app only looks at beacons whose region UUID is the same as promixityID.

The init() function initialises the member variables locationManager and proximityID. The UUID assigned to proximityID is the one used by my sample app. You should generate a different UUID for your app. Finally, init() calls initLocating(), which initialises beacon ranging.

initLocating() first checks whether your iPhone supports beacon ranging at all. Beacons are supported by iOS 7.0 and the iPhone 5S or newer. If beacon ranging is available on the user’s iPhone, the function first requests when-in-use authorisation for accessing location services. The user is bothered with a prompt only when the app is started the first time after its installation. The app should also check whether the user granted the authorisation request, which I leave out for brevity.

If the iPhone doesn’t support beacon ranging or if the user doesn’t grant access to the location services, the app should ideally fall back to a mode not relying on location services. This is not always possible with navigation apps for cars as the prime example. An audio guide for museums could fall back to entering the numbers of the exhibits manually.

The desiredAccuracy is set to kCLLocationAccuracyThreeKilometers (3 kilometres). This is the coarsest possible accuracy, which can easily be achieved with the help of cell towers. Wifi or GPS are not needed. Hence, there is no additional drain on the iPhone’s battery by Wifi or GPS.

The crucial setting for enabling beacon ranging in the background is the line

    self.locationManager.allowsBackgroundLocationUpdates = true

Finally, we tell the CLLocationManager to deliver location updates to AGLocationManager by making AGLocationManager the delegate of CLLocationManager. This ensures that locationManager(_:didRangeBeacons:inRegion:) gets called every time any property of any beacon in range changes or if the beacons in range change.

Step 4: Starting and Stopping Beacon Ranging

Now, everything is prepared to receive location updates. The final step is to start location updates and beacon ranging explicitly.

    func startLocating() {
        self.locationManager.startUpdatingLocation()
        self.locationManager.startRangingBeaconsInRegion(CLBeaconRegion(proximityUUID: self.proximityID, identifier: "AllBeacons"))
    }

Note that beacon ranging will not work in the background if you do not call startUpdatingLocation().

Location updates and beacon ranging are stopped with the following function.

    func stopLocating() {
        self.locationManager.stopRangingBeaconsInRegion(CLBeaconRegion(proximityUUID: self.proximityID, identifier: "AllBeacons"))
        self.locationManager.stopUpdatingLocation()
    }

Apple recommends to start location updates and beacon ranging only when it is really needed – to reduce power consumption and to extend battery life. In my sample AudioGuide app, I stop beacon ranging when audio is playing. As soon as the audio playback stops, the app restarts beacon ranging. Then, the visitor of the museum is most likely walking to the next exhibit.

Now, your app will happily receive location updates in the function locationManager(_:didRangeBeacons:inRegion:). I’ll show how to determine the phone’s location in a museum in a future post.