Skip to content

3. Detecting beacons

One of the main job of Kontakt.io iOS SDK is helping your app to be aware of nearby beacons. Implementation of this feature varies slightly, depending on what type of beacons (iBeacon or Eddystone) you want to use with your app. It's caused by a different level of native support by iOS itself.

In general it boils down to 4 steps:

  1. Setting up a manager object.
  2. Asking a user for required permissions.
  3. Starting a discovery.
  4. Handling detected beacons in manager's delegate methods.

This process is described in details in next sections.

iBeacon monitoring and ranging

Prerequisites

First step

Make sure that Kontakt.io iOS SDK has been added to your project. Instructions on how to do this are available in the Installation section.

Please also don't forget that you need to add reference to Kontakt.io iOS SDK in every file that you want to use it through an import statement:

import KontaktSDK

Beacon manager

KTKBeaconManager is the main class responsible for handling iBeacon monitoring and ranging. Under the hood it refers to CLLocationManager. Because of that it's recommended to make it an instance property in your view controller.

import UIKit
import KontaktSDK

class ViewController: UIViewController {

    var beaconManager: KTKBeaconManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        beaconManager = KTKBeaconManager(delegate: self)
    }

}

Please remember that the delegate that you specify when instantiating your beacon manager must conform to KTKBeaconManagerDelegate protocol.

Permissions

iBeacon support on iOS is a part of Location Services. Kontakt.io iOS SDK, when it comes to monitoring and ranging, is built on top of Core Location. Because of this, before you start monitoring you need to make sure your app has the necessary permission to use Location Services and ask for them if needed.

Information about the authorization stat is returned by KTKBeaconManager's class method locationAuthorizationStatus() and you request a permission by calling one of your beacon manager's methods, either requestLocationAlwaysAuthorization or requestLocationWhenInUseAuthorization, depending on your use case.

override func viewDidLoad() {
    super.viewDidLoad()
    beaconManager = KTKBeaconManager(delegate: self)

    switch KTKBeaconManager.locationAuthorizationStatus() {
    case .notDetermined:
        beaconManager.requestLocationAlwaysAuthorization()
    case .denied, .restricted:
        // No access to Location Services
    case .authorizedWhenInUse:
        // For most iBeacon-based app this type of
        // permission is not adequate
    case .authorizedAlways:
        // We will use this later
    }
}

Don't forget to put the correct key in your info.plist - If you call one of these method without including its corresponding key, the system ignores your request.

Please also keep in mind that authorization for your app can change over time. The right place to handle those changes is KTKBeaconManagerDelegate method beaconManager:didChangeLocationAuthorizationStatus:

extension ViewController: KTKBeaconManagerDelegate {
    func beaconManager(_ manager: KTKBeaconManager, didChangeLocationAuthorizationStatus status: CLAuthorizationStatus) {
        if status == .authorizedAlways {
            // When status changes to CLAuthorizationStatus.authorizedAlways
            // e.g. after calling beaconManager.requestLocationAlwaysAuthorization()
            // we can start region monitoring from here
        }
    }
}

Beacon region

As mentioned before, beacon monitoring and ranging is a type of Location Services. In order to set up monitoring and ranging, you need to set up a region. By doing this you inform the system in what beacons your app is interested in and should be notified about.

Beacon regions are represented in Kontakt.io iOS SDK by KTKBeaconRegion class. You can instantiate new object of this class by using one of three initializers, depending on which iBeacon identifiers you want to use for your region.

let myProximityUuid = UUID(uuidString: "f7826da6-4fa2-4e98-8024-bc5b71e0893e")
let region = KTKBeaconRegion(proximityUUID: myProximityUuid!, identifier: "Beacon region 1")

Region monitoring

When you have a region ready, you can start monitoring it. It means iOS will notify your app when user's devices crosses region's boundary (in either direction). Please remember that region, in this case, is defined by iBeacon identifiers. Crossing a boundary means entering or exiting a range of a beacon broadcasting identifiers specified in region you monitor.

Start monitoring

To start monitoring call your beacon manager's startMonitoringForRegion: method, providing your region object as a parameter. But before you do this, it's a good idea to check whether a user granted your app the necessary permissions and does user's device is capable of monitoring beacons, for example (using the switch statement from previous sample:

switch KTKBeaconManager.locationAuthorizationStatus() {
// Non-relevant cases are cut
case .authorizedAlways:
    if KTKBeaconManager.isMonitoringAvailable() {
        beaconManager.startMonitoring(for: region)
    }
}

Handling monitoring callbacks

Region monitoring events are handled by KTKBeaconManagerDelegate. The object that you've specified when during your beacon manager initialization must conform to this protocol. The delegate can implement a number of methods related to region monitoring, although none of them is required.

extension ViewController: KTKBeaconManagerDelegate {
    func beaconManager(_ manager: KTKBeaconManager, didStartMonitoringFor region: KTKBeaconRegion) {
        // Do something when monitoring for a particular
        // region is successfully initiated
    }

    func beaconManager(_ manager: KTKBeaconManager, monitoringDidFailFor region: KTKBeaconRegion?, withError error: NSError?) {
        // Handle monitoring failing to start for your region
    }

    func beaconManager(_ manager: KTKBeaconManager, didEnter region: KTKBeaconRegion) {
        // Decide what to do when a user enters a range of your region; usually used
        // for triggering a local notification and/or starting a beacon ranging
    }

    func beaconManager(_ manager: KTKBeaconManager, didExitRegion region: KTKBeaconRegion) {
        // Decide what to do when a user exits a range of your region; usually used
        // for triggering a local notification and stoping a beacon ranging
    }
}

Useful tools

Basic apps usually use a single beacon region, but there are use cases that require more. Your beacon manager's monitoredRegions property gives you all regions that are being monitored.

You don't need to stop region monitoring one-by-one. There is a beacon manager method that will take care of stopping them all at once - stopMonitoringForAllRegions.

If you need to determine whether you are inside or outside of a region, beacon manager's requestStateForRegion: method. Beacon manager will check in what state is a device and will notify your delegate via this method:

extension ViewController: KTKBeaconManagerDelegate {
    func beaconManager(_ manager: KTKBeaconManager, didDetermineState state: CLRegionState, for region: KTKBeaconRegion) {
        // Do something depending on a value of the state argument
    }
}

Beacon ranging

Region monitoring will only let you know when you've entered or exited a range of any beacon that belongs to a region. Sometimes that's all your app will need to know, but sometimes you may want to have detailed information about beacons in range. That's when the beacon ranging comes into play.

Start and stop beacon ranging

It's only possible to range beacons belonging to a certain region. Because of that a good practice is to start beacon ranging when you enter a region and end it when you exit that region. That's why KTKBeaconManagerDelegate methods didEnterRegion and didExitRegion are the perfect place to use beacon manager object to respectively start and stop beacon ranging.

extension ViewController: KTKBeaconManagerDelegate {
    func beaconManager(_ manager: KTKBeaconManager, didEnter region: KTKBeaconRegion) {
        manager.startRangingBeacons(in: region)
    }

    func beaconManager(_ manager: KTKBeaconManager, didExitRegion region: KTKBeaconRegion) {
        manager.stopRangingBeacons(in: region)
    }
}

Handling ranging data

Once you start ranging beacons every second your app will receive updates about ranged beacons from your region. Your beacon manager delegate can catch this data through didRangeBeacons:inRegion: delegate method. A sample implementation might look like this:

extension ViewController: KTKBeaconManagerDelegate {
    func beaconManager(_ manager: KTKBeaconManager, didRangeBeacons beacons: [CLBeacon], in region: KTKBeaconRegion) {
        for beacon in beacons {
            print("Ranged beacon with Proximity UUID: \(beacon.proximityUUID), Major: \(beacon.major) and Minor: \(beacon.minor) from \(region.identifier) in \(beacon.proximity) proximity")
        }
    }
}

Ranged beacons are represented as CLBeacon objects. They contain information about all beacon identifying values (Proximity UUID, Major and Minor) as well as data about range to that beacon (proximity and accuracy properties) and raw signal strength (rssi property). To learn more about beacon object we strongly recommend consulting official Apple documentation for CLBeacon class.

Battery consumption and differences between background and foreground ranging

Region monitoring, for all intents and purposes, should be considered as having no impact on battery life. That's why once started, it can be active all the time, no matter in what state your app currently is.

Unfortunately beacon ranging is more resource-intensive, which automatically means less battery-friendly. If you want to provide a good experience for users of your app take a special care in making sure your app does not range when it doesn't need to.

Because of this power consumption issue, iOS will significantly restrict a possibility of beacon ranging in the background. If your app is actively ranging and is put into the background, the ranging will stop within few seconds.

There is, however, a situation where your app can range in the background, although for a short period of time (about 10 seconds) - it's when it receives a notification about entering a beacon region. When this happens your app can work for a brief moment as if it was in foreground.

Additionally, if you need more time for ranging in either of the cases, e.g. if you want to trigger a notification only if someone gets into a certain proximity to your beacon and you want to download a remote content for that notification, you can always use UIApplication's beginBackgroundTaskWithExpirationHandler: or beginBackgroundTaskWithName:expirationHandler: methods to set up a background task that will allow your app to finish its job. That should give up to 3 minutes of additional time. More information is available in Apple's UIApplication class documentation and Background Execution Programming Guide.

Additional resources

Before you start making your own iBeacon-based apps we recommend some further reading:

Detecting Eddystone beacons

Eddystone introduction

Eddystone is a Bluetooth beacon standard introduced by Google in July 2015. It is actively developed and we support it on our beacons as well as in Kontakt.io iOS SDK.

Eddystone standard is a bit more complex, comparing to iBeacon, mainly because of multiple type advertising packets, that have different roles. For more information we recommend reading What is Eddystone? article from Kontakt.io blog and then checking official Eddystone specification.

Since Apple does not provide native support for Eddystone protocol, Kontakt.io iOS SDK is the best way to add Eddystone-awareness to your app.

Prerequisites

First step

Make sure that Kontakt.io iOS SDK has been added to your project. Instructions on how to do this are available in the Installation section.

Please also don't forget that you need to add a reference to Kontakt.io iOS SDK in every file that you want to use it through an import statement:

import KontaktSDK

Eddystone region

Eddystone regions have the same purpose as their iBeacon counterparts - to allow your app to distinguish between your beacons and all other beacons that your users might encounter.

There are four ways to define a new Eddystone region:

let eddystoneRegion1 = KTKEddystoneRegion(namespaceID: "f7826da6bc5b71e0893e")    
let eddystoneRegion2 = KTKEddystoneRegion(namespaceID: "f7826da6bc5b71e0893e", instanceID: "5869696c7373") // why is instanceID an optional?
let eddystoneRegion3 = KTKEddystoneRegion(url: URL(string: "http://kntk.io/eddystone")!)
let eddystoneRegion4 = KTKEddystoneRegion(urlDomain: "kontakt.io")

Eddystone manager

Since Core Location is not aware of Eddystone beacons, Kontakt.io iOS SDK has a separate manager for detecting Eddystone beacons. In order to start working with Eddystone beacons you need to initialize a KTKEddystoneManager as a class variable e.g. of your view controller and instantiate it with a delegate object that conforms to KTKEddystoneManagerDelegate protocol.

class ViewController: UIViewController {

    var eddystoneManager: KTKEddystoneManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        eddystoneManager = KTKEddystoneManager(delegate: self)
    }

}

extension ViewController: KTKEddystoneManagerDelegate {
    // implement KTKEddystoneManagerDelegate protocol
}

Discovering & getting updates from Eddystone beacons

When your Eddystone beacons manager is ready you can start looking for nearby Eddystone beacons. However, it's a good practice to check whether Bluetooth is available on user's device before you do that. Similarly like with iBeacon monitoring, you can specify in what region your manager should look for your Eddystone beacons, but contrary to iBeacon, that region can be nil. If you do that, your Eddystone manager will be notified about all Eddystone beacons in range of your iPhone/iPad.

if eddystoneManager.centralState == CBCentralManagerState.poweredOn {
    // Discover only certain beacons...
    eddystoneManager.startEddystoneDiscovery(in: eddystoneRegion1)
    // ...or all posible Eddystone beacons in range
    // eddystoneManager.startEddystoneDiscovery(in: nil)
}

didDiscoverEddystones:inRegion: and didUpdateEddystone:withFrame:

Please forgive us, but a documentation for these methods is still a work in progress.