Add proximity events to an iOS app

If you want to trigger events in your app based on proximity to places and objects, you’re in the right place!

In this tutorial, we’ll build a simple iOS app with Estimote Proximity SDK. The app will receive “enter” and “exit” events whenever it’s in proximity to the beacons.

Tip: If you’d rather dive into ready-made code, check out the example apps in Estimote Cloud.

What’s ahead (aka Table of Contents)

Prerequisites

  • 1 x Mac computer with Xcode 9.
    • Proximity SDK is a Swift 4 framework, so it won’t work with earlier versions of Xcode.
  • 1 x iPhone (4S or newer) or iPad (3rd gen or newer) to run the application.
  • 1 x Estimote Account (sign up here).
  • 1 or more Estimote Proximity or Location Beacons.

Configure your beacons

Starting in mid-September 2017, Estimote Beacons ship with Estimote Monitoring, the backbone of the Proximity SDK, enabled by default. You’re already good to go!

If you got your beacons before that, enabling Estimote Monitoring is easy:

  1. Get the “Estimote” app from the App Store.
  2. Go to “Configuration”.
  3. Find your beacon on the radar.
  4. Log in with your Estimote Account.
  5. Go to “Broadcasting”, then “Estimote Monitoring”, and flip the switch!

Prepare an Xcode project

Start by creating a new Xcode project using the “Single View Application” template. You can pick Swift or Objective‑C, Proximity SDK works perfectly fine with both. Name it however you want—how about a classic “HelloWorld”?

Add Proximity SDK via CocoaPods

The easiest way to hook the project up with the Proximity SDK is via CocoaPods. Launch the Terminal app and type in the following commands: (a command is everything after the $ sign)

$ cd Path/To/HelloWorld
$ sudo gem install cocoapods # using Homebrew's Ruby? then skip "sudo"
# if you get an error, you can also try:
$ sudo gem install -n /usr/local/bin cocoapods

$ echo -e 'target "HelloWorld" do\n  pod "EstimoteProximitySDK"\nend' > Podfile
# if you didn't name your project HelloWorld, change the line above accordingly
$ pod install --repo-update

Now, close Xcode and reopen your project, but use the .xcworkspace file this time, just as instructed by the output from the pod install command.

If you still have the terminal open, you can simply type open HelloWorld.xcworkspace.

Note: If you prefer not to use CocoaPods, here are the manual installation instructions.

Enable Bluetooth Background Mode

To allow our app to run in the background when in range of beacons, we need to enable the Bluetooth Background Mode in our Xcode project.

  1. Select the project in the left sidebar, and on the right, go to “Capabilities”.
  2. Flip the Background Modes to “ON”.
  3. Expand the Background Modes section and check the “Uses Bluetooth LE accessories” option.

Keep in mind: No other Background Modes are required! If you enable any unnecessary Background Modes, you risk your app being rejected during App Store review, so don’t be checking boxes “just in case” (-:

Set the Location Services usage description

Proximity SDK also requires iBeacon Location Services to work in the background, which means we need to set up Location Services usage description. This description will be shown to the user when asking them about allowing the app to access their location.

In the project navigator, find the Info.plist file, right click on it, and select “Open As”, “Source Code”. Then, inside the top-level <dict> section, add:

<key>NSLocationWhenInUseUsageDescription</key>
<string>We'll show you cool things near you in the app.</string>

<!-- iOS 10 or earlier -->
<key>NSLocationAlwaysUsageDescription</key>
<string>We'll show you cool things near you in the app, and alert you via
notifications if you don't have the app open.</string>

<!-- iOS 11 -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We'll show you cool things near you in the app. With the "always" option,
we can also alert you via notifications if you don't have the app open.</string>

Why the three keys and what do they mean?

  • NSLocationWhenInUseUsageDescription should describe how your app uses Location Services when it’s in use (or “in the foreground”).

  • NSLocationAlwaysUsageDescription should describe how your app uses Location Services both when in use, and in the background. This description is only for users of your app with iOS 10 or earlier. The user can agree to the “always” authorization, or disable Location Services in the app completely.

  • NSLocationAlwaysAndWhenInUseUsageDescription should describe how your app use Location Services both when in use, and in the background. This description is only for users of your app with iOS 11. The user can select between the “always” or “only when in use” authorizations, or disable Location Services in the app completely.

Add a Proximity Observer

The general rule of thumb for where to put the Proximity Observer is:

  • For events which you want to handle in the app’s UI, feel free to put it straight in your View Controller, or wherever matches your app’s architecture.

  • For events which you want to handle in the background, such as showing notifications, put it in the AppDelegate. This is because:

    • your app will likely get killed by iOS at some point while away from the beacons, due to inactivity and to free up memory for other apps
    • the next time the app comes into range of beacons, iOS will restart it into the background to handle the proximity events
    • however, apps restarted into the background don’t have their View Controllers instantiated (they’re not needed in the background)
    • AppDelegate on the other hand will always be created, and have its application:didFinishLaunching delegate method called

In this tutorial, let’s assume we’re doing the latter, and add the Proximity Observer to the AppDelegate.

Tip: If you want to go with the first option, just follow the tutorial, but use the ViewController file instead of AppDelegate, and the viewDidLoad method instead of application:didFinishLaunching.

Import the Proximity SDK

First, we have to import the Proximity SDK. In the AppDelegate.swift or AppDelegate.m file:

import UIKit
// add this:
import EstimoteProximitySDK
#import "AppDelegate.h"
// add this:
@import EstimoteProximitySDK;

Set up Estimote Cloud credentials

This will allow the SDK to communicate with Estimote Cloud on your behalf.

You can generate a token for yourself on cloud.estimote.com/#/apps/add. Pick “Your Own App”, give it a name, and voilà!

Once you have your App ID and Token, add this one line in the AppDelegate, inside the didFinishLaunchingWithOptions method:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // add this:
    let cloudCredentials = CloudCredentials(appID: "<#App ID#>",
                                            appToken: "<#App Token#>")
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // add this:
    EPXCloudCredentials *cloudCredentials =
    [[EPXCloudCredentials alloc] initWithAppID:@"<#App ID#>"
                                      appToken:@"<#App Token#>"];

Remember to replace the placeholders, naturally!

Create a Proximity Observer object

We’re now ready to create our Proximity Observer object:

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    // 1. Add a property to hold the Proximity Observer
    var proximityObserver: ProximityObserver!

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        let cloudCredentials = // ...

        // 2. Create the Proximity Observer
        self.proximityObserver = ProximityObserver(
            credentials: cloudCredentials,
            onError: { error in
                print("proximity observer error: \(error)")
        })

        return true
    }

// ...
@interface AppDelegate ()
// 1. Add a property to hold the Proximity Observer
@property (nonatomic) EPXProximityObserver *proximityObserver;
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    EPXCloudCredentials *cloudCredentials = // ...

    // 2. Create the Proximity Observer
    self.proximityObserver = [[EPXProximityObserver alloc]
                              initWithCredentials:cloudCredentials
                              onError:^(NSError * _Nonnull error) {
        NSLog(@"proximity observer error = %@", error);
    }];

    return YES;
}

// ...

Define Proximity Zones

Time to tell our Proximity Observer what enter/exit events we’re interested in! But first, some theory:

Tags and attachments

Chance is, knowing when the user enters range of beacon 1b4fe is not that useful in and on itself. However, if your app somehow knows that beacon 1b4fe is placed on Peter’s desk … “In range of beacon 1b4fe” suddenly means “close to Peter’s desk”.

In other words, most of the time, proximity to beacons only makes sense if you can give your beacons some meaning. That’s what beacon tags and attachments are for … an easy way for you to attach some extra data/meaning to a beacon, for example:

"identifier": "1b4fe",
"tag": "desks",
"attachments": {
   "desk-owner": "Peter"
 }

With such a setup, it’s now easy to say “monitor proximity to desks”, and also figure out whose desk we’re close to.

And since tags and attachments are stored in Estimote Cloud, it’s also easy to change the setup without having to change the app’s code. For example, if sombebody else takes Peter’s desk, just change the “desk-owner” to a new one. If your app was coded to welcome the owner at their desk by their name, it’ll now start using the new one.

Set up tags and attachments

With the theory out of the way, let’s set up some tags and attachments.

Go to cloud.estimote.com, select one of your beacons, and click “Edit”. Then:

  • to set up a tag: Click on “Tags”, “Create New Tag”, enter “desks”, and click “Add”.
  • to set up attachments: On the left side, select “Beacon Attachment”. Then, add a “desk-owner” key with value set to “Peter”.

Finish with the “Save Changes” at the bottom. Ta-da, we’ve just set up our first beacon!

Repeat these steps for another beacon. Use the same “desks” tag, but this time, set the “desk-owner” to “Alex”.

Create Proximity Zone objects

Now we can move on to creating Proximity Zone objects in our app. Back to the future AppDelegate!

self.proximityObserver = // ...

// add this below:
let zone = ProximityZone(tag: "desks", range: .near)
zone.onEnter = { context in
    let deskOwner = context.attachments["desk-owner"]
    print("Welcome to \(deskOwner)'s desk")
}
zone.onExit = { _ in
    print("Bye bye, come again!")
}
self.proximityObserver = // ...

// add this below:
EPXProximityZone *zone = [[EPXProximityZone alloc]
                          initWithTag: @"desks"
                          range: EPXProximityRange.nearRange];
zone.onEnter = ^(id<EPXProximityZoneContext> context) {
    NSString *deskOwner = context.attachments[@"desk-owner"];
    NSLog(@"Welcome to %@'s desk", deskOwner);
};
zone.onExit = ^(id<EPXProximityZoneContext> context) {
    NSLog(@"Bye bye, come again!");
};

Note that for this setup to work, you need to spread the beacons apart a good few meters. If they overlap, then moving from one beacon to the other is considered moving within the zone, and it won’t trigger additional enter/exit actions.

If you want to know about movements inside a zone spanned by multiple overlapping beacons, you can use the “onChange” action instead. Think about it as: I’m still in the same desks zone (hence no new enter/exits), but my context (which specific desks are in range) has changed (hence the “onChange” action).

zone.onContextChange = { contexts in
    let deskOwners: [String] = contexts.map { context in
        return context.attachments["desk-owner"]!
    }
    print("In range of desks: \(deskOwners)")
}
zone.onContextChange = ^(NSSet<id<EPXProximityZoneContext>> contexts) {
    NSMutableArray *desksOwners = [NSMutableArray new];
    for (id<EPXProximityZoneContext> *context in contexts) {
        [desksOwners addObject:context.attachments[@"desk-owner"]];
    }
    NSLog(@"In range of desks: %@", desksOwners);
};

Here’s how this would work in an overlapping scenario:

# move in range of "Peter's desk" beacon
# this is also when the "enter" action would get called
In range of desks: [Peter]
# move in range of both beacons
In range of desks: [Peter, Alex]
# move out of range of "Peter's desk", but still in range of "Alex's desk"
In range of desks: [Alex]
# move out of range of both beacons
# this is also when the "exit" action would get called
In range of desks: []

About the Proximity Zone range

There are predefined far (5 meters) and near (1 meter) ranges, but you can also define your own:

ProximityRange(desiredMeanTriggerDistance: 3.5)
[[EPXProximityRange alloc] initWithDesiredMeanTriggerDistance: 3.5]

We call it a software-defined range. Note that this is independent of the beacon’s physical broadcasting range. By default, Estimote Beacons have a physical range north of 50 meters. This sets the upper limit of what you can define for your Proximity Zones, and you can boost it further by increasing the beacon’s Broadcasting Power. (This comes at the expense of the battery life, so only do this if you need to.)

You can even have more than one enter/exit zone per beacon:

let innerZone = ProximityZone(
    tag: "treasure",
    range: ProximityRange(desiredMeanTriggerDistance: 3.0)!)

innerZone.onEnter = { _ in print("Hot!") }
innerZone.onExit = { _ in print("Colder...") }

let outerZone = ProximityZone(
    tag: "treasure",
    range: ProximityRange(desiredMeanTriggerDistance: 9.0)!)

outerZone.onEnter = { _ in print("Warmer...") }
outerZone.onExit = { _ in print("Brrr, freezing!") }

With this setup, when the user gets closer to the beacon, they’ll get a “warmer” message at 10 meters away, and “hot” at 3 meters. When moving away from the beacon, they’ll get a “colder” message at 3 meters and “freezing” at 10 meters.

Important: “Desired mean trigger distance” is quite a mouthful, but that’s because we really want to emphasize that distance estimations based on Bluetooth signal strength are pretty rough. The actual trigger range may vary based on the beacon placement, the environment, and many other factors.

Tip: In general, the closer to the beacon, the more accurate the trigger. That is, a 3-meter trigger should be more precise and consistent than a 30-meter trigger. It’s worth to keep that in mind when planning your proximity zones and the placement of your beacons.

Start proximity observation

Phew, many lines of code later … Luckily, all that’s left is to say “start!”

self.proximityObserver.startObserving([zone])
[self.proximityObserver startObservingZones:@[zone]];

You can now run the app on your device, move your beacons around, and observe the Xcode console logs.