Getting Started with iBeacon: A Swift Tutorial

Posted on .

New! This tutorial is available as a free eBook. You can pay whatever you'd like (including $0), and all the content is the same (with a few small exceptions). Get the eBook

This tutorial will show you how to write an iOS app that changes the color of the screen to match the color of the closest beacon. We'll be working with CoreLocation, and writing everything in Swift for iOS8 (as of 10/24/15 this has been updated for iOS 9 and Xcode 7.1). Although there are some nice SDKs out there, we aren't using anything but pure Swift for this. I want to show you how little code you actually need to get a proof-of-concept iBeacon app like this up and running. Plus, it's always good to have an understanding of what those SDKs are doing for you behind the scenes.

All the code for this demo is available on GitHub here: closest-beacon-demo, and here is a screencast of the tutorial, if you'd rather watch than read:

Requirements

If you'd like to follow along, here's what you'll need:

  • Xcode 6 or newer, so you can follow along with the Swift code.
  • A BLE (Bluetooth Low Energy) iOS device (iPhone 4S or newer, iPod touch 5th gen or newer, iPad 3 or newer, or iPad mini) running iOS8 or later (we're writing in Swift). Make sure Bluetooth is turned on. You must use a real device (the Simulator cannot detect beacons).
  • Beacons. I'm using Estimote beacons, but you can use anything you'd like. You can even download an app that will turn your device into a beacon, but it will need to be a different device than the one you're running this demo on.

Project Setup

To get started, create a new project and choose the Single View Application template.

Xcode Project Template Screen

On the project options screen, make sure you choose Swift as the language. You may choose whatever you'd like in the 'Devices' dropdown. You will need to load the app on a real device, so choose whichever applies to you.

Xcode Project Options Screen

CoreLocation & CLLocationManager

iBeacon technology is part of the CoreLocation framework, so the first thing we need to do is to include CoreLocation. Start by opening up ViewController.swift (where all the code is going for this example), and adding the import right at the top:

import UIKit
import CoreLocation

In order to use CoreLocation, we need to create an instance of a Core Location Manager, or CLLocationManager. Add this to the top of the ViewController class:

class ViewController: UIViewController {
    let locationManager = CLLocationManager()
    ...
}

The locationManager will handle all the details of looking for the beacons and reporting its findings. It does this through delegate methods, so next we need to set up our View Controller to listen for these methods. First, let the View Controller know which delegate methods it will have available by telling it to follow the CLLocationManagerDelegate protocol:

class ViewController: UIViewController, CLLocationManagerDelegate {

Next, in viewDidLoad(), we need to let our locationManager know that this View Controller should be its delegate (where it should deliver its messages). Add this line inside of viewDidLoad():

override func viewDidLoad() {
    super.viewDidLoad()

    locationManager.delegate = self;
}

Now as soon as our locationManager starts working, we'll be able to access the results right from inside our View Controller. But there are a few more things we need to set up before it'll be able to start working.

Requesting Permission for Location Services

Any time we use location services in iOS, the user needs to approve the request. To request authorization, call one of two methods on our locationManager instance: locationManager.requestWhenInUseAuthorization(), or locationManager.requestAlwaysAuthorization(). The 'when in use' authorization only allows you to use location services while your app is in the foreground, whereas the 'always' authorization lets you access location services at any time, even waking up and starting your app in response to some event. For our purposes, we're just going to request the 'when in use' authorization. Add this line right after the last one:

override func viewDidLoad() {
    super.viewDidLoad()

    locationManager.delegate = self;
    locationManager.requestWhenInUseAuthorization()
}

The next step is to define the message that will explain why the user should approve the request. This is the text that will fill up the main section of the popup window that the system presents to the user. Open up your info.plist file (inside of the 'Supporting Files' folder) and add a new line by hovering over an existing line and clicking the circled '+' button.

Adding a new entry in the info.plist file

Add the key as 'NSLocationWhenInUseUsageDescription' (this will not auto-complete for you). Its type is a String, and you can fill in anything you'd like for the value.

Now, you don't need to actually call this requestWhenInUseAuthorization() method every time the app runs, the user only needs to approve it once and your app will stay authorized. But if they go into settings and revoke your app's authorization, then you will need to ask again to be able to access location services again. Let's wrap our call in a conditional so that we only ask for authorization when we need it.

You can access your app's authorization status through the authorizationStatus() class method on CLLocationManager. There are are few different cases when we may wish to request authorization, so let's just check to request authorization whenever the status isn't authorized for 'when in use'. Wrap our previous authorization request so it looks like this:

if (CLLocationManager.authorizationStatus() != CLAuthorizationStatus.AuthorizedWhenInUse) {
    locationManager.requestWhenInUseAuthorization()
}

Now, if the user denies the authorization, our app can't do anything. In a full-fledged responsible app, we would account for that denial with a message explaining that the app won't be able to do its magic, and perhaps give the user another chance to authorize. For this demo, we're going to keep it simple and just ignore that scenario.

So now we have everything we need to get access to Location Services while our app is running. Go ahead and run the app and you'll see that you get a popup with the message we put in our info.plist requesting authorization.

Our demo app requesting Location Services authorization

Note: this is as far as you'll be able to get with the Simulator. From here on, you'll need to run the app on a real device (see the list of BLE iOS devices near the top of this tutorial).

Ok, now that we have our locationManager set up, we have our View Controller delivering delegate methods, and our app has authorization to access Location Services, we're ready to start looking for beacons!

Regions

Our locationManager looks for beacons via regions. A region (CLBeaconRegion) defines which beacons our locationManager should care about. Each region defines a unique identifier (UUID) of the beacons which it should 'see'. There may be many beacons around, some of which aren't yours, and by defining a region with an identifier unique to your beacons, you can filter out the noise. For a refresher on how iBeacon uses UUID, Major, and Minor values, check out my earlier post on What is iBeacon.

To define a region, you'll need to know the UUID of the beacons you are using. I'm using Estimote beacons, and I'm using the default UUID that they ship with. Add the following line near the top of our View Controller, swapping out the UUIDString with your own:

let locationManager = CLLocationManager()
let region = CLBeaconRegion(proximityUUID: NSUUID(UUIDString: "B9407F30-F5F8-466E-AFF9-25556B57FE6D")!, identifier: "Estimotes")

The identifier is just an arbitrary string that you can use to reference a region. When beacons come back from our delegate method, they are delivered with the identifier of the region they were found in. This would be helpful if you were to monitor multiple regions, but since we aren't, I just set the identifier to "Estimotes" (the brand of beacon I'm currently using).

Now that we have a region, we just need to have our locationManager start monitoring that region. A good place to do this is after we get authorization to use location services:

locationManager.delegate = self;
if (CLLocationManager.authorizationStatus() != CLAuthorizationStatus.AuthorizedWhenInUse) {
    locationManager.requestWhenInUseAuthorization()
}
locationManager.startRangingBeaconsInRegion(region)

Finding Beacons

Next up we want to be notified when a relevant beacon is found. For this, we'll use the didRangeBeacons:inRegion delegate method. Add this block of code (if you start typing 'locationManager', the delegate methods will pop up to auto-complete):

func locationManager(manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], inRegion region: CLBeaconRegion) {
    println(beacons)
}

The println is just going to log out the array of beacons that are found. Now is a good time to run the app on a device and keep an eye on the debug area in Xcode. You should see an array of beacons printing out at regular intervals.

"Debug area showing arrays of ranged beacons"

Inside that delegate method, we have access to the beacons that the locationManager has found. This method is called every time:

  • A new beacon comes within range
  • A beacon goes out of range (this will come into play soon!)
  • A beacon gets closer or farther away

Since the signal strength that gets reported from beacons tend to fluctuate quite a bit, you can count on this delegate method being called as often as a few times per second if there are beacons nearby.

Working with Beacons

Now, finally, we get to really dive in and talk about beacons! The delegate method is going to hand us an array of CLBeacon instances, each with a few properties that we'll need to use. The Major and Minor values are used to identify each beacon. In my case, the Estimotes all have different Major and Minor values. We'll use the Minor value to determine the color of the beacon:

  • Blue: 31351
  • Purple: 54482
  • Green: 27327

We're going to use these later, so let's put them in an array at the top of our View Controller. I'm using the minor values as keys (we'll reference these later), and I've come up with RGB values that are close to the color of my beacons. Be sure to replace the keys with the minor values of the beacons that you are using.

let locationManager = CLLocationManager()
let region = CLBeaconRegion(proximityUUID: NSUUID(UUIDString: "B9407F30-F5F8-466E-AFF9-25556B57FE6D")!, identifier: "Estimotes")
let colors = [
    54482: UIColor(red: 84/255, green: 77/255, blue: 160/255, alpha: 1),
    31351: UIColor(red: 142/255, green: 212/255, blue: 220/255, alpha: 1),
    27327: UIColor(red: 162/255, green: 213/255, blue: 181/255, alpha: 1)
]

There are three other values on each beacon: proximity, accuracy and rssi.

proximity

proximity is an enum (CLProximity) with four possible values, 0-3, relating to how close the beacon is. This is a good, simple way to determine placement of beacons in a general sense, when you only need to know whether you are far away (3), near (2), or right next to (1) a beacon. Unknown values are reported as 0.

You should expect to have multiple beacons return with the same proximity value. If you need to get more specific (like we do), proximity won't be precise enough.

rssi

rssi is a measurement (in decibels) of the signal strength of the beacon. This will be a negative whole number, and the bigger the number (the closer to 0), the stronger the signal. Unknown values are reported as 0.

accuracy

accuracy is a real-world number that you can actually understand. But it comes with a catch: don't trust it. accuracy is a measurement in meters of the distance from the beacon, with unknown values reported as negative numbers (always -1 in my experience).

The reason you shouldn't trust the meter measurement is that it is very unlikely to ever be accurate. There are a lot of factors that affect a Bluetooth signal, and this value is only guessed at by measuring signal strength. It isn't reliable enough to be used as a ruler or measuring tape, but could be used in a more general sense that accounts for a hefty margin of error.

Unknown Values

You will notice though that in all these cases, unknown values are still reported. For our purposes, we want to ignore any beacons with unknown values, so let's filter them out from the array in the delegate method. Back to writing code!

We'll use the CLProximity enum to find and strip out the unknown beacons from the returned array. Apply the filter method on the array like this:

func locationManager(manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], inRegion region: CLBeaconRegion) {
    let knownBeacons = beacons.filter{ $0.proximity != CLProximity.Unknown }
}

Here, we're creating a new array by filtering the beacons array that the didRangeBeacons method gave us. In the filter method, we're basically letting anything through as long as its proximity value doesn't match CLProximity.Unknown.

The Closest Beacon

Now for a note on the beacons array, and our new knownBeacons array. From my testing, the beacons arrive in the beacons array already sorted from closest to farthest, with unknowns coming in first, before the closest known beacon. That said, I can't find anywhere in the documentation mentions this, so I can't recommend relying on this to always happen in a production application. For the sake of simplicity here, we're going to just grab the first element in the beacons array as the closest beacon.

func locationManager(manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], inRegion region: CLBeaconRegion) {
    let knownBeacons = beacons.filter{ $0.proximity != CLProximity.Unknown }
    if (knownBeacons.count > 0) {
        let closestBeacon = knownBeacons[0] as CLBeacon
    }
}

Next, we want to change the color of the background. We'll use the colors array for this, using the minor value of the closest beacon as the key for the color:

func locationManager(manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], inRegion region: CLBeaconRegion) {
    let knownBeacons = beacons.filter{ $0.proximity != CLProximity.Unknown }
    if (knownBeacons.count > 0) {
        let closestBeacon = knownBeacons[0] as CLBeacon
        self.view.backgroundColor = self.colors[closestBeacon.minor.integerValue]
    }
}

And that's it! Run the app and start moving beacons closer and farther away from your device, and watch the background color change.

The demo app in action

If it isn't working, here are a few things to double-check:

  • You loaded the app onto a device, not the simulator. The simulator won't detect beacons.
  • You're using a BLE device (see a list near the top of the post) and Bluetooth is turned on.
  • You approved the request to use Location Services. If you accidentally hit Deny, you can either delete the app, or go into the Settings app and approve it manually.
  • You updated your region to use the UUID of your beacons.
  • In the colors array, you are using the minor values of your beacons for the keys.