Important: This guide describes the “monolithic” Estimote SDK, from  Estimote/iOS-SDK

The proximity detection features in that SDK are now obsoleted in favor of the new Estimote Proximity SDK, available at  Estimote/iOS-Proximity-SDK

To learn more about the new API, see the Add Proximity to iOS app guide instead.

Part 3: Ranging beacons

In part 2, we’ve learned how to monitor beacons in our app in order to be notified whenever the device enters and exits range of these beacons. The great part? It works even if the app is not running, which we’ve demonstrated by showing a notification on the lock screen on “enter” event.

Monitoring is very powerful in certain ways, but it’s more of a coarse-grained mechanism. It operates on the entire sets of beacons—beacon regions—and is limited to up to 20 of such regions.

Fortunately, beacon monitoring is complemented by another iBeacon feature: ranging beacons. We’ll use this one in our airport app with the intent to guide users to nearby snack bars. (Don’t know about you, but we’re always hungry before our flights, and Yelp or Foursquare have a tendency to recommend food spots located just before the security checkpoint we’ve just passed.)

What’s ahead (aka Table of Contents)

What is ranging?

While monitoring creates a virtual fence to detect when you’re moving in and out, ranging actively scans for any nearby beacons and delivers results to you every second.

Let’s say we defined a “terminal Z” region. With monitoring, our app will be notified whenever the user enters and exits the terminal. But if we start ranging for the exact same region, we’ll instead get a full list of matching beacons currently in range—complete with their UUID, major, and minor values.

This is especially important if you recall that an app can only monitor for up to 20 regions at a time—making it impossible to monitor for, e.g., every single gate, even on a mid-sized airport. There’s, however, no limit to the number of regions that can be ranged.

Finally, there’s one more distinct feature to ranging—one that deserves its own paragraph.

Proximity estimation

Each beacon broadcasts its Bluetooth signal with a certain strength—a strength which diminishes as the signal travels through the air. This enables the receiver device to make a rough estimation of how far the beacon is. Strong signal = it’s close. Weak signal = it’s further away.

Info: It’s actually an inverse-square relationship, i.e., if a distance to the beacon increases two times, the signal strength decreases four times. This makes the accuracy of the proximity estimations decrease drastically as the distance increases. (Signal strength at 20 meters will be roughly 100x weaker than at 2 meters!)

Ranging utilizes the differences in received signal strength to:

  • (a) sort the list of beacons detected during ranging, starting with beacons likely closer, to those probably further away,
  • (b) categorize the beacons into four proximity zones:
    • immediate (strong signal; usually up to a few centimeters)
    • near (medium signal; usually up to a few meters)
    • far (weak signal; more than a few meters)
    • unknown (“hard to say”, usually when the signal is very, very weak)

Important: Contrary to many opinions circulating the Internet, ranging does not provide an estimated distance to a beacon! The common theory that the accuracy property of a CLBeacon is Apple’s crypto-alias for “distance” has no strong evidence to support it.

More importantly, while received signal strength, proximity zone and accuracy values can theoretically be used to derive a distance estimation, in practice this is far from trivial and requires complex mathematical models to account for fluctuations in the signal strength.

Long story short: do not expect distance estimations from beacons.

Tip: If your use case necessitates more precise positioning data, try our Indoor Location technology instead. It uses a mix of beacons and other sensors, and complex maths to tie them together, to provide you with (x,y) position of the device inside an indoor space.

The trade-offs

Ranging provides more granular and comprehensive beacon data, but this comes with certain trade-offs.

  1. First and foremost, ranging uses up more energy than monitoring—although still less than GPS. This means that it’s usually not a good idea to run ranging for extended periods of time, e.g., hours. It certainly wouldn’t be viable to run it at all times, even with the app shut down, which brings us to the second trade-off.

  2. Ranging works only when the app is active. As soon as the app transitions to a suspended state, ranging pauses until the app becomes active again.

    Note however the subtle difference between “active” and “in the foreground.” If the app is launched into the background by beacon monitoring, it’s “active”—and it can start or continue ranging beacons for the short amount of time before iOS puts the app back to the “suspended” state.

Tip: Because ranging works only when the app is running, it also only requires the “when in use” level of authorization to access Location Services. In part one, we opted for the “always” authorization, but if you don’t plan to use monitoring, consider the “when in use” level for your apps instead.

Apart from calling requestWhenInUseAuthorization instead of requestAlwaysAuthorization, you’ll also need to rename the key in your Info.plist file to NSLocationWhenInUseUsageDescription.

Pro tip: While a bit harder to execute properly, you can actually support both levels of authorization, and let the user decide. NSHipster has a great write-up going into more details: Requesting Multiple Permissions.

Start ranging

Starting ranging is very similar to starting monitoring—we also need to provide a beacon region that will define which beacons to scan for. Let’s say we’re interested in all the beacons installed at the airport. For that, we can use the broadest variant of defining a beacon region—by UUID only.

Ideally, you’d assign a dedicated UUID to all of the airport beacons—but for the purposes of this tutorial, let’s just use the default Estimote UUID: B9407F30-F5F8-466E-AFF9-25556B57FE6D. (If you’ve changed the UUID since you got your beacons, you’ll need to adjust the following code appropriately.)

Since our “food places nearby” feature is tied to a particular screen, we’ll cut a corner and put a second beacon manager in a View Controller corresponding to the screen. This’ll allow us to manipulate the view directly in the ranging delegate. (A more idiomatic way would be to have the beacon manager we already have in the AppDelegate to manipulate a data model, and the View Controller observing the changes to the data model instead. We just want to keep thing simple in this tutorial.)

Keep in mind: While it’s safe to have multiple beacon managers to handle different ranging use cases, it’s a bit more tricky with monitoring. Monitored regions are shared system resources, which means that all beacon managers will receive “enter” and “exit” events for all regions monitored by the app—regardless of which particular beacon manager started monitoring these regions.

This can be both annoying (two different managers interfering with each other) and convenient (start monitoring in one manager, pick up the events in another), just make sure to remember about it.

Let’s go to the ViewController implementation file and set up a second beacon manager. Also, this time, we’ll create a dedicated property to hold the beacon region, since we’ll be using it in two places: to start, and to stop ranging.

import UIKit

// 1. Add the ESTBeaconManagerDelegate protocol
class ViewController: UIViewController, ESTBeaconManagerDelegate  {

    // 2. Add the beacon manager and the beacon region
    let beaconManager = ESTBeaconManager()
    let beaconRegion = CLBeaconRegion(
        proximityUUID: UUID(uuidString: "B9407F30-F5F8-466E-AFF9-25556B57FE6D")!,
        identifier: "ranged region")

    override func viewDidLoad() {
        super.viewDidLoad()
        // 3. Set the beacon manager's delegate
        self.beaconManager.delegate = self
        // 4. We need to request this authorization for every beacon manager
        self.beaconManager.requestAlwaysAuthorization()
    }

// ...
#import "ViewController.h"

// 1. Add an import
#import <EstimoteSDK/EstimoteSDK.h>

// 2. Add the ESTBeaconManagerDelegate protocol
@interface ViewController () <ESTBeaconManagerDelegate>
// 3. Add properties to hold the beacon manager and the beacon region
@property (nonatomic) ESTBeaconManager *beaconManager;
@property (nonatomic) CLBeaconRegion *beaconRegion;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 4. Instantiate the beacon manager & set its delegate
    self.beaconManager = [ESTBeaconManager new];
    self.beaconManager.delegate = self;
    // 5. Instantiate the beacon region
    self.beaconRegion = [[CLBeaconRegion alloc]
        initWithProximityUUID:[[NSUUID alloc]
            initWithUUIDString:@"B9407F30-F5F8-466E-AFF9-25556B57FE6D"]
        identifier:@"ranged region"];
    // 6. We need to request this authorization for every beacon manager
    [self.beaconManager requestAlwaysAuthorization];
}

// ...

Now, the code to start and stop ranging as the view controller appears and disappears on screen. This goes inside the ViewController class:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.beaconManager.startRangingBeacons(in: self.beaconRegion)
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    self.beaconManager.stopRangingBeacons(in: self.beaconRegion)
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.beaconManager startRangingBeaconsInRegion:self.beaconRegion];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self.beaconManager stopRangingBeaconsInRegion:self.beaconRegion];
}

Tying beacon data and app data together

Our monitoring use case was quite trivial—just show a notification. With ranging, our goal is to show nearby options to a grab a snack, which involves tying two pieces of data together: identifiers of nearby beacons, and a list of food options. (In general, since beacons only broadcast their identifiers, this is something you’ll end up doing quite often when developing beacon apps.)

First, we need a data structure to hold the food options, beacons, and the distances between the two.

Second, we need to code up a simple algorithm:

  1. Take the closest beacon.
  2. Look up all the food places and the distances between them, and the beacon.
  3. Sort the food places by the distance.

Again, to keep this tutorial short and about beacons, we’re going to cut more corners:

  • We’ll use a hard-coded list of beacons, food places and distances between them. Ideally, we’d fetch this from a web API of some sorts.
  • We’ll stop at computing the sorted list of food places to present to the user, leaving the implementation of the UI to the reader. Hint, hint, table view? (-:

Let’s start by adding the data:

class ViewController: UIViewController, ESTBeaconManagerDelegate {

    // Add the property holding the data.
    // TODO: replace "<major>:<minor>" strings to match your own beacons
    let placesByBeacons = [
        "6574:54631": [
            "Heavenly Sandwiches": 50, // read as: it's 50 meters from
                                       // "Heavenly Sandwiches" to the beacon with
                                       // major 6574 and minor 54631
            "Green & Green Salads": 150,
            "Mini Panini": 325
        ],
        "648:12": [
            "Heavenly Sandwiches": 250,
            "Green & Green Salads": 100,
            "Mini Panini": 20
        ],
        "17581:4351": [
            "Heavenly Sandwiches": 350,
            "Green & Green Salads": 500,
            "Mini Panini": 170
        ]
    ]

    // ...
@interface ViewController () <ESTBeaconManagerDelegate>
// 1. Add the property to hold the data.
@property (nonatomic) NSDictionary *placesByBeacons;
// ...
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 2. Populate the data.
    // TODO: replace "<major>:<minor>" strings to match your own beacons
    self.placesByBeacons = @{
        @"6574:54631": @{
            @"Heavenly Sandwiches": @50, // read as: it's 50 meters from
                                         // "Heavenly Sandwiches" to the beacon with
                                         // major 6574 and minor 54631
            @"Green & Green Salads": @150,
            @"Mini Panini": @325
        },
        @"648:12": @{
            @"Heavenly Sandwiches": @250,
            @"Green & Green Salads": @100,
            @"Mini Panini": @20
        },
        @"17581:4351": @{
            @"Heavenly Sandwiches": @350,
            @"Green & Green Salads": @500,
            @"Mini Panini": @170
        }
    };

    self.beaconManager = [ESTBeaconManager new];
    // ...

We went for two nested dictionaries. The outer one maps beacon’s major and minor (clumped together in a <major>:<minor> string) to the inner one. The inner one maps names of food places to their distance from the beacon. All in all, nothing too sophisticated or even elegant, but for this simple example it’ll do.

Remember to replace the majors and minors with those of your beacons!

Info: We intentionally omitted the UUID of beacons, since we specified it as part of the beacon region definition we’re passing to the startRanging method.

We’ll now implement a method that takes a CLBeacon object representing the closest beacon, and return a list of all the places sorted by their distance to the beacon:

func placesNearBeacon(_ beacon: CLBeacon) -> [String]? {
    let beaconKey = "\(beacon.major):\(beacon.minor)"
    if let places = self.placesByBeacons[beaconKey] {
        let sortedPlaces = Array(places).sorted { $0.1 < $1.1 }.map { $0.0 }
        return sortedPlaces
    }
    return nil
}
- (NSArray *)placesNearBeacon:(CLBeacon *)beacon {
    NSString *beaconKey = [NSString stringWithFormat:@"%@:%@",
                           beacon.major, beacon.minor];
    NSDictionary *places = [self.placesByBeacons objectForKey:beaconKey];
    NSArray *sortedPlaces = [places keysSortedByValueUsingComparator:
                             ^NSComparisonResult(id obj1, id obj2) {
                                 return [obj1 compare:obj2];
                             }];
    return sortedPlaces;
}

Ranging delegate

Recall that the list of ranged beacons is already sorted from the (likely) nearest to the (likely) furthest ones.

Info: Yes, we’re very persistent with the likely keyword. All the proximity estimations for beacons are based on the signal strength, which is naturally susceptible to fluctuations. It will occasionally happen that a beacon further away is considered closer than the actual nearest beacon, or even show up in the ranging results before the nearest one does.

Temporarily anomalies don’t hurt our airport app—the order of places will simply be off for a moment. However, if you’re building a museum app which automatically starts a new audio guide every time the closest beacon changes to a new one, it’s better to take ranging results with a pinch of salt.

Having the list pre-sorted by the Estimote SDK, and with all the prep work we’ve performed, the ranging delegate turns out to be quite simple:

func beaconManager(_ manager: Any, didRangeBeacons beacons: [CLBeacon],
                   in region: CLBeaconRegion) {
    if let nearestBeacon = beacons.first,
       let places = placesNearBeacon(nearestBeacon) {
        // TODO: update the UI here
        print(places) // TODO: remove after implementing the UI
    }
}
- (void)beaconManager:(id)manager didRangeBeacons:(NSArray *)beacons
             inRegion:(CLBeaconRegion *)region {
    CLBeacon *nearestBeacon = beacons.firstObject;
    if (nearestBeacon) {
        NSArray *places = [self placesNearBeacon:nearestBeacon];
        // TODO: update the UI here
        NSLog(@"%@", places); // TODO: remove after implementing the UI
    }
}

That’s it! (Apart from adding the UI of course.) Run the app on your device, spread the beacons around and move the phone from one to another. The order of places printed in the console should keep changing depending on which beacon is the closest.

Key takeaways

  • Beacon ranging provides fine-grained data about beacons detected nearby, as opposed to monitoring’s coarse-grained “inside region” and “outside region.” The data includes exact UUID, major, and minor values of ranged beacons, as well as proximity estimations.

  • Proximity estimations are based on received signal strength, and are good to roughly determine if the device is close to or far away from a beacon. Beacons are not meant to provide distance estimations!

  • Ranging works only when the app is running, and only requires the “when in use” authorization to access Location Services.

  • Use startRangingBeaconsInRegion and stopRangingBeaconsInRegion to control ranging. Ranging results are delivered every second to the didRangeBeacons delegate method. Ranging results is an array of CLBeacon objects, sorted from the beacons likely closest to the device to those likely further away.