Add Indoor Location to an iOS app

In this guide, we’ll learn how to build a simple app which uses beacons and Estimote Indoor Location to obtain precise (x,y) coordinates of its user inside an indoor space.

Tip: If you’d rather dive into ready-made code, there are some example projects bundled with the iOS Indoor SDK. We still recommend going through this guide for the extra context and understanding!

What’s ahead (aka Table of Contents)

Prerequisites

  • 1 x Mac computer with Xcode.
  • 1 x iPhone 5 (or newer).
  • 1 x Estimote Account (sign up here).
  • 4 or more Estimote Location Beacons.

Map your space out

If you haven’t yet, start by mapping the space which we’ll later use for indoor positioning in our app.

With the map of the space ready and waiting for us in the cloud, let’s move on to building the app itself.

Set up a new project

We’ll start by creating an Xcode project using the “Single View Application” template. Pick the language of your choice, name the app (e.g., “HelloIndoor”) and save the new project.

Install Indoor SDK via CocoaPods

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

$ cd Path/To/HelloIndoor
$ 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 "HelloIndoor" do\n  pod "EstimoteIndoorSDK"\nend' > Podfile
# if you didn't name your project HelloIndoor, 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 HelloIndoor.xcworkspace.

Important: Remember to keep using the .xcworkspace file from now on. If you open the .xcodeproj instead, you’ll get a “linker command failed with exit code 1” and “library not found for -lPods” errors.

Manual Indoor SDK installation

If you prefer not to use CocoaPods, you can install the Indoor SDK manually. Just follow the instructions at: https://github.com/Estimote/iOS-Indoor-SDK#installation

Swift users: add an Objective-C bridging header

Indoor SDK is written in Objective-C. It still works perfectly fine with Swift projects, but you need to take the additional step of adding a bridging header:

  1. Right-click on the project’s group in the project navigator, and choose “New File…”
  2. Pick a “Header File” from the “iOS - Source” section, and save it as ObjCBridge.h.
  3. Add the following imports to the newly created file:

    #import "EILIndoorSDK.h"
    #import <EstimoteSDK/EstimoteSDK.h>
    
  4. Select your project in the navigator and go to “Build Settings.”
  5. Find the “Objective-C Bridging Header” setting and set it to ${PROJECT_NAME}/ObjCBridge.h.

Add Indoor Location Manager

Indoor Location Manager is the centerpiece of the Indoor SDK. Let’s add it to our project.

import UIKit

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

    // 2. Add the location manager
    let locationManager = EILIndoorLocationManager()

    override func viewDidLoad() {
        super.viewDidLoad()
        // 3. Set the location manager's delegate
        self.locationManager.delegate = self
    }

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

// 1. Add imports
#import "ESTConfig.h"
#import "EILIndoorLocationManager.h"
#import "EILRequestFetchLocation.h"
#import "EILOrientedPoint.h"

// 2. Add the EILIndoorLocationManagerDelegate protocol
@interface ViewController () <EILIndoorLocationManagerDelegate>
// 3. Add a property to hold the location manager
@property (nonatomic) EILIndoorLocationManager *locationManager;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 4. Instantiate the location manager & set its delegate
    self.locationManager = [EILIndoorLocationManager new];
    self.locationManager.delegate = self;
}

// ...

Connect the app to Estimote Cloud

We’ll now connect our app with the Estimote Cloud to be able to download the map of the location that we created earlier.

First, go to Estimote Cloud, the “Apps” tab, and add a new “Your Own App”. It’ll give you the App ID and App Token, necessary for the app we’re building to be able to access the Cloud API.

Now, add the following code to the viewDidLoad method:

ESTConfig.setupAppID("<App ID>", andAppToken: "<App Token>")
[ESTConfig setupAppID:@"<App ID>" andAppToken:@"<App Token>"];

Fetch location from Estimote Cloud

First, let’s add a property to the ViewController class to hold the location object that we’ll be fetching from the cloud:

// find this line:
let locationManager = EILIndoorLocationManager()
// add this one below it:
var location: EILLocation!
// find this line:
@property (nonatomic) EILIndoorLocationManager *locationManager;
// add this one below it:
@property (nonatomic) EILLocation *location;

Now, let’s add code to actually fetch the location, putting this at the bottom of the viewDidLoad method:

let fetchLocationRequest = EILRequestFetchLocation(locationIdentifier: "my-kitchen")
fetchLocationRequest.sendRequest { (location, error) in
    if location != nil {
        self.location = location!
    } else {
        print("can't fetch location: \(error)")
    }
}
EILRequestFetchLocation *fetchLocationRequest =
        [[EILRequestFetchLocation alloc] initWithLocationIdentifier:@"my-kitchen"];
[fetchLocationRequest sendRequestWithCompletion:^(EILLocation *location,
                                                  NSError *error) {
    if (location != nil) {
        self.location = location;
    } else {
        NSLog(@"can't fetch location: %@", error);
    }
}];

Don’t forget to change the “my-kitchen” identifier to that of your own location. You’ll find the identifier on the list of your locations in Estimote Cloud.

Start location updates

With the location object safely stored in the location property, we can now start indoor positioning:

// this is where we left off:
self.location = location!
// add this right below:
self.locationManager.startPositionUpdates(for: self.location)
// this is where we left off:
self.location = location;
// add this right below:
[self.locationManager startPositionUpdatesForLocation:self.location];

Finally, let’s add the delegate methods to receive the indoor positioning updates:

func indoorLocationManager(manager: EILIndoorLocationManager!,
                           didFailToUpdatePositionWithError error: NSError!) {
    print("failed to update position: \(error)")
}

func indoorLocationManager(manager: EILIndoorLocationManager!,
                           didUpdatePosition position: EILOrientedPoint!,
                           withAccuracy positionAccuracy: EILPositionAccuracy,
                           inLocation location: EILLocation!) {
    var accuracy: String!
    switch positionAccuracy {
        case .veryHigh: accuracy = "+/- 1.00m"
        case .high:     accuracy = "+/- 1.62m"
        case .medium:   accuracy = "+/- 2.62m"
        case .low:      accuracy = "+/- 4.24m"
        case .veryLow:  accuracy = "+/- ? :-("
        case .unknown:  accuracy = "unknown"
    }
    print(String(format: "x: %5.2f, y: %5.2f, orientation: %3.0f, accuracy: %@",
        position.x, position.y, position.orientation, accuracy))
}
-    (void)indoorLocationManager:(EILIndoorLocationManager *)manager
didFailToUpdatePositionWithError:(NSError *)error {
    NSLog(@"failed to update position: %@", error);
}

- (void)indoorLocationManager:(EILIndoorLocationManager *)manager
            didUpdatePosition:(EILOrientedPoint *)position
                 withAccuracy:(EILPositionAccuracy)positionAccuracy
                   inLocation:(EILLocation *)location {
    NSString *accuracy;
    switch (positionAccuracy) {
        case EILPositionAccuracyVeryHigh: accuracy = @"+/- 1.00m"; break;
        case EILPositionAccuracyHigh:     accuracy = @"+/- 1.62m"; break;
        case EILPositionAccuracyMedium:   accuracy = @"+/- 2.62m"; break;
        case EILPositionAccuracyLow:      accuracy = @"+/- 4.24m"; break;
        case EILPositionAccuracyVeryLow:  accuracy = @"+/- ? :-("; break;
        case EILPositionAccuracyUnknown:  accuracy = @"unknown"; break;
    }
    NSLog(@"x: %5.2f, y: %5.2f, orientation: %3.0f, accuracy: %@",
          position.x, position.y, position.orientation, accuracy);
}

That’s it! Run the app, and after a short while that it takes to fetch the location and to initialize all the Indoor Location subsystems, you should start seeing coordinates floating down the console window.

Location updates in the background

It’s very similar to location updates in the foreground! There’s just three things to keep in mind.

First, you need to have Location Background Mode enabled on your beacons. To do that, connect to the beacons with the Estimote App, go to “Packets”, find “Estimote Location” and turn “Background Mode” on.

Second, use the EILBackgroundIndoorLocationManager and EILBackgroundIndoorLocationManagerDelegate instead of the EILIndoorLocationManager and EILIndoorLocationManagerDelegate. Plus, it only makes sense to put that Background Location Manager in your AppDelegate. This will ensure that the positioning continues to work if your app got killed and later re-launched into the background.

Finally, you need to set up your app for running in the background and accessing user’s location at all times:

  • In your project’s settings, on the “Capabilities” tab, enable the “Background Modes” and check the “Uses Bluetooth LE accessories” option.
  • In your Info.plist file, add a value for key “Privacy - Location Always Usage Description” (or “NSLocationAlwaysUsageDescription”).
  • Call the requestAlwaysAuthorization on your Background Location Manager. This will trigger a pop-up with the message from your Info.plist file, asking the user to agree for your app to access their location at all times.

Here’s a complete example:

class AppDelegate: UIResponder, UIApplicationDelegate,
        EILBackgroundIndoorLocationManagerDelegate {

let backgroundIndoorManager = EILBackgroundIndoorLocationManager()

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    ESTConfig.setupAppID("<#App ID#>", andAppToken: "<#App Token#>")

    self.backgroundIndoorManager.delegate = self
    self.backgroundIndoorManager.requestAlwaysAuthorization()

    let fetchLocation = EILRequestFetchLocation(locationIdentifier: "my-kitchen")
    fetchLocation.sendRequest { (location, error) in
        if let location = location {
            self.backgroundIndoorManager.startPositionUpdates(for: location)
        } else {
            print("can't fetch location: \(error)")
        }
    }
}

func backgroundIndoorLocationManager(
        _ locationManager: EILBackgroundIndoorLocationManager,
        didFailToUpdatePositionWithError error: Error) {
    print("failed to update position: \(error)")
}

func backgroundIndoorLocationManager(
        _ manager: EILBackgroundIndoorLocationManager,
        didUpdatePosition position: EILOrientedPoint,
        with positionAccuracy: EILPositionAccuracy,
        in location: EILLocation) {
    // ...
}

// ...
#import "ESTConfig.h"
#import "EILBackgroundIndoorLocationManager.h"

@interface AppDelegate () <EILBackgroundIndoorLocationManagerDelegate>
@property (nonatomic) EILBackgroundIndoorLocationManager *backgroundIndoorManager;
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [ESTConfig setupAppID:@"<#App ID#>" andAppToken:@"<#App Token#>"];

    self.backgroundIndoorManager = [EILBackgroundIndoorLocationManager new];
    self.backgroundIndoorManager.delegate = self;
    [self.backgroundIndoorManager requestAlwaysAuthorization];

    EILRequestFetchLocation *fetchLocation =
        [[EILRequestFetchLocation alloc] initWithLocationIdentifier:@"my-kitchen"];
    [fetchLocation sendRequestWithCompletion:^(EILLocation *location,
                                               NSError *error) {
        if (location != nil) {
            [self.backgroundIndoorManager startPositionUpdatesForLocation:location];
        } else {
            NSLog(@"can't fetch location: %@", error);
        }
    }];
}

- (void)backgroundIndoorLocationManager:(EILBackgroundIndoorLocationManager *)manager
       didFailToUpdatePositionWithError:(NSError *)error {
    NSLog(@"failed to update position: %@", error);
}

- (void)backgroundIndoorLocationManager:(EILBackgroundIndoorLocationManager *)manager
                      didUpdatePosition:(EILOrientedPoint *)position
                           withAccuracy:(EILPositionAccuracy)positionAccuracy
                             inLocation:(EILLocation *)location {
    // ...
}

// ...

Two closing notes:

  • If you want to use Indoor Location in both foreground and background, you’ll need both managers.
  • In the background, position will be updated every 5 seconds.

Next steps

You can use the coordinates obtained from Indoor Location to:

  • Trigger actions in certain parts of the space, e.g.:

    let coffeeMachine = EILPoint(x: 3.1, y: 7.2)
    if position.distanceToPoint(coffeeMachine) < 5 {
        // start brewing coffee
    }
    
    EILPoint *coffeeMachine = [[EILPoint alloc] initWithX:3.1 y:7.2];
    if ([position distanceToPoint:coffeeMachine] < 5) {
        // start brewing coffee
    }
    
  • Store the coordinates or send them to an external server for further analysis, e.g., to create a heatmap.

  • Draw a map of the space and mark user’s position with an avatar.

    You can do that with the EILIndoorLocationView and EILPositionView classes. We even have a ready-made example of this: IndoorMap

Also, feel free to study the iOS Indoor SDK reference to learn more about the methods exposed by the Indoor SDK.