iBeacon Tutorial with iOS and Swift
Learn how you can find an iBeacon around you, determine its proximity, and send notifications when it moves away from you. By Owen L Brown.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iBeacon Tutorial with iOS and Swift
30 mins
Listening for Your iBeacon
Now that your app has the location permissions it needs, it’s time to find those beacons! Add the following class extension to the bottom of ItemsViewController.swift :
// MARK: - CLLocationManagerDelegate extension ItemsViewController: CLLocationManagerDelegate { }
This will declare ItemsViewController
as conforming to CLLocationManagerDelegate
. You’ll add the delegate methods inside this extension to keep them nicely grouped together.
Next, add the following line inside of viewDidLoad()
:
locationManager.delegate = self
This sets the CLLocationManager
delegate to self
so you’ll receive delegate callbacks.
Now that your location manager is set up, you can instruct your app to begin monitoring for specific regions using CLBeaconRegion
. When you register a region to be monitored, those regions persist between launches of your application. This will be important later when you respond to the boundary of a region being crossed while your application is not running.
Your iBeacon items in the list are represented by the the Item
model via the items
array property. CLLocationManager
, however, expects you to provide a CLBeaconRegion
instance in order to begin monitoring a region.
In Item.swift create the following helper method on Item
:
func asBeaconRegion() -> CLBeaconRegion {
return CLBeaconRegion(proximityUUID: uuid,
major: majorValue,
minor: minorValue,
identifier: name)
}
This returns a new CLBeaconRegion
instance derived from the current Item
.
You can see that the classes are similar in structure to each other, so creating an instance of CLBeaconRegion
is very straightforward since it has direct analogs to the UUID, major value, and minor value.
Now you need a method to begin monitoring a given item. Open ItemsViewController.swift and add the following method to ItemsViewController
:
func startMonitoringItem(_ item: Item) {
let beaconRegion = item.asBeaconRegion()
locationManager.startMonitoring(for: beaconRegion)
locationManager.startRangingBeacons(in: beaconRegion)
}
This method takes an Item
instance and creates a CLBeaconRegion
using the method you defined earlier. It then tells the location manager to start monitoring the given region, and to start ranging iBeacons within that region.
Ranging is the process of discovering iBeacons within the given region and determining their distance. An iOS device receiving an iBeacon transmission can approximate the distance from the iBeacon. The distance (between transmitting iBeacon and receiving device) is categorized into 3 distinct ranges:
- Immediate Within a few centimeters
- Near Within a couple of meters
- Far Greater than 10 meters away
Far
, Near
, and Immediate
are not specifically documented, but this Stack Overflow Question gives a rough overview of the distances you can expect.
By default, monitoring notifies you when the region is entered or exited regardless of whether your app is running. Ranging, on the other hand, monitors the proximity of the region only while your app is running.
You’ll also need a way to stop monitoring an item’s region after it’s deleted. Add the following method to ItemsViewController
:
func stopMonitoringItem(_ item: Item) {
let beaconRegion = item.asBeaconRegion()
locationManager.stopMonitoring(for: beaconRegion)
locationManager.stopRangingBeacons(in: beaconRegion)
}
The above method reverses the effects of startMonitoringItem(_:)
and instructs the CLLocationManager
to stop monitor and ranging activities.
Now that you have the start and stop methods, it’s time to put them to use! The natural place to start monitoring is when a user adds a new item to the list.
Have a look at addBeacon(_:)
in ItemsViewController.swift. This protocol method is called when the user hits the Add button in AddItemViewController
and creates a new Item
to monitor. Find the call to persistItems()
in that method and add the following line just before it:
startMonitoringItem(item)
That will activate monitoring when the user saves an item. Likewise, when the app launches, the app loads persisted items from UserDefaults
, which means you have to start monitoring for them on startup too.
In ItemsViewController.swift, find loadItems()
and add the following line inside the for
loop at the end:
startMonitoringItem(item)
This will ensure each item is being monitored.
Now you need to take care of removing items from the list. Find tableView(_:commit:forRowAt:)
and add the following line inside the if
statement:
stopMonitoringItem(items[indexPath.row])
This table view delegate method is called when the user deletes the row. The existing code handles removing it from the model and the view, and the line of code you just added will also stop the monitoring of the item.
At this point you’ve made a lot of progress! Your application now starts and stops listening for specific iBeacons as appropriate.
You can build and run your app at this point; but even though your registered iBeacons might be within range your app has no idea how to react when it finds one…time to fix that!
Acting on Found iBeacons
Now that your location manager is listening for iBeacons, it’s time to react to them by implementing some of the CLLocationManagerDelegate
methods.
First and foremost is to add some error handling, since you’re dealing with very specific hardware features of the device and you want to know if the monitoring or ranging fails for any reason.
Add the following two methods to the CLLocationManagerDelegate
class extension you defined earlier at the bottom of ItemsViewController.swift:
func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
print("Failed monitoring region: \(error.localizedDescription)")
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location manager failed: \(error.localizedDescription)")
}
These methods will simply log any received errors as a result of monitoring iBeacons.
If everything goes smoothly in your app you should never see any output from these methods. However, it’s possible that the log messages could provide very valuable information if something isn’t working.
The next step is to display the perceived proximity of your registered iBeacons in real-time. Add the following stubbed-out method to the CLLocationManagerDelegate
class extension:
func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
// Find the same beacons in the table.
var indexPaths = [IndexPath]()
for beacon in beacons {
for row in 0..<items.count {
// TODO: Determine if item is equal to ranged beacon
}
}
// Update beacon locations of visible rows.
if let visibleRows = tableView.indexPathsForVisibleRows {
let rowsToUpdate = visibleRows.filter { indexPaths.contains($0) }
for row in rowsToUpdate {
let cell = tableView.cellForRow(at: row) as! ItemCell
cell.refreshLocation()
}
}
}
This delegate method is called when iBeacons come within range, move out of range, or when the range of an iBeacon changes.
The goal of your app is to use the array of ranged iBeacons supplied by the delegate methods to update the list of items and display their perceived proximity. You'll start by iterating over the beacons
array, and then iterating over items
to see if there are matches between in-range iBeacons and the ones in your list. Then the bottom portion updates the location string for visible cells. You'll come back to the TODO
section in just a moment.
Open Item.swift and add the following property to the Item
class:
var beacon: CLBeacon?
This property stores the last CLBeacon
instance seen for this specific item, which is used to display the proximity information.
Now add the following equality operator at the bottom of the file, outside the class definition:
func ==(item: Item, beacon: CLBeacon) -> Bool {
return ((beacon.proximityUUID.uuidString == item.uuid.uuidString)
&& (Int(beacon.major) == Int(item.majorValue))
&& (Int(beacon.minor) == Int(item.minorValue)))
}
This equality function compares a CLBeacon
instance with an Item
instance to see if they are equal — that is, if all of their identifiers match. In this case, a CLBeacon
is equal to an Item
if the UUID, major, and minor values are all equal.
Now you'll need to complete the ranging delegate method with a call to the above helper method. Open ItemsViewController.swift and return to locationManager(_:didRangeBeacons:inRegion:)
. Replace the TODO
comment in the innermost for
loop with the following:
if items[row] == beacon {
items[row].beacon = beacon
indexPaths += [IndexPath(row: row, section: 0)]
}
Here, you set the cell's beacon
when you find a matching item and iBeacon. Checking that the item and beacon match is easy thanks to your equality operator!
Each CLBeacon
instance has a proximity
property which is an enum
with values of far
, near
, immediate
, and unknown
.
Add the following method to Item
:
func nameForProximity(_ proximity: CLProximity) -> String {
switch proximity {
case .unknown:
return "Unknown"
case .immediate:
return "Immediate"
case .near:
return "Near"
case .far:
return "Far"
}
}
This returns a human-readable proximity value from proximity
which you'll use next.
Still in Item
, add the following method:
func locationString() -> String {
guard let beacon = beacon else { return "Location: Unknown" }
let proximity = nameForProximity(beacon.proximity)
let accuracy = String(format: "%.2f", beacon.accuracy)
var location = "Location: \(proximity)"
if beacon.proximity != .unknown {
location += " (approx. \(accuracy)m)"
}
return location
}
This generates a nice, neat string describing not only the proximity range of the beacon, but also the approximate distance.
Now it's time to use that new method to display the perceived proximity of the ranged iBeacon.
Open ItemCell.swift and add the following to just below the lblName.text = item.name
line of code:
lblLocation.text = item.locationString()
This displays the location for each cell's beacon. And to ensure it shows updated info, add the following inside refreshLocation()
:
lblLocation.text = item?.locationString() ?? ""
refreshLocation()
is called each time the locationManager
ranges the beacon, which sets the cell's lblLocation.text
property with the perceived proximity value and approximate 'accuracy' taken from the CLBeacon
.
This latter value may fluctuate due to RF interference even when your device and iBeacon are not moving, so don't rely on it for a precise location for the beacon.
Now ensure your iBeacon is registered and move your device closer or away from your device. You'll see the label update as you move around, as shown below:
You may find that the perceived proximity and accuracy is drastically affected by the physical location of your iBeacon; if it is placed inside of something like a box or a bag, the signal may be blocked as the iBeacon is a very low-power device and the signal may easily become attenuated.
Keep this in mind when designing your application — and when deciding the best placement for your iBeacon hardware.