Introduction
This blog post provides a tutorial on how to configure two iBeacons and have an iOS app detect those two iBeacons and react to them.
The purpose of the app is to detect proximity to an iBeacon (which could be a product in a warehouse, for example). The product should then be added to a list in the app.
Setup
For this tutorial, the setup is
- 2 iBeacons
- one iOS device
- Xcode
- A tea or coffee (I prefer tea 😉 )
Furthermore, you should have basic knowledge about IBOutlets and how to connect them to a controller.
Configure the iBeacons
The configuration of an iBeacon is quite easy. You can use normal iBeacons or if you have more iOS devices you can download an app which will work as an iBeacon. It is important that you know the uuid, major and minor values, and the identifier of the iBeacons.
Create Xcode project
To create a new Xcode project, open Xcode and select create a new Xcode project. On the next screen select single view application and click next. At the next screen fill out the product name with Inventory. Inventory will be the name of our app. Select as language swift and as devices iPhone and click on next.
The new Xcode project is created and should contain a Main.storyboard
file, open that file and add a label and a table view. In the table view add a table view cell. The next step is to add two labels to our table view cell. The first label is called title and the second one amount.
After that create a new swift class which is called InventarCell and another swift struct which is called Inventar.
The implementation for the InventarCell is shown below:
import UIKit
class InventarCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var amountLabel: UILabel!
var inventar: Inventar! {
didSet {
titleLabel.text = inventar.title
if let amountString = inventar.amount{
amountLabel.text = "\(amountString)"
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
This class is a representation of our table view cell in the Main.storyboard
file. But before we can take use our custom implementation we need to link the table view cell to our class. To do that open the Main.storyboard
file and select the table view cell. Go to the attribute inspector and change the style to custom and set the identifier to InventarCell. Also open the identity inspector and set as custom class our new created InventarCell
class. We are nearly done with our custom table view cell the last step is to add the outlets for the title and amount label.
The next implementation is the Inventar struct:
import UIKit
struct Inventar {
var title: String?
var amount: Int?
init(title: String, amount: Int) {
self.title = title
self.amount = amount
}
}
Nothing special is going to happen here. We create a struct with a title and an amount and the struct contains an init function to create a new inventar object.
Configure the ViewController
Let us start with the view controller. First set the dataSource
and delegate of the table view to the view controller. Furthermore, the table view needs to be set as an IBOutlet
in the view controller. We are also going to create an empty set of the type Inventar
which will hold the data to be shown on the table view. A set is being used because with an array it could occur that a product would be added twice or more and we don’t want that. After the declaration of a set, an error will occur saying the inventar structure needs to implement Hashable
. Change your inventar struct to this:
import UIKit
struct Inventar: Hashable {
var title: String?
var amount: Int?
var hashValue : Int {
get {
return "\(String(describing: self.title))".hashValue
}
}
init(title: String, amount: Int) {
self.title = title
self.amount = amount
}
}
// function to compare the hashValue
func ==(lhs: Inventar, rhs: Inventar) -> Bool {
return lhs.hashValue == rhs.hashValue
}
Our view controller is still empty that will be changed now. At first import one library which is necessary to detect iBeacons. The name of the library is CoreLocation
.
Furthermore, our view controller is the datasource and delegate of the table view. Let us extend our ViewController
with UITableViewDelegate
and UITableViewDataSource
. After the extension, we need to implement the functions of the table view in the view controller which are:
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return inventar.count
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// cell selected code here
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "InventarCell", for: indexPath) as! InventarCell
let product = inventar.first!
cell.inventar = product
return cell
}
To detect beacons with the app, the view controller needs to extend CLLocationManagerDelegate
. The delegate includes to implement one method which icalled when a beacon is detected. The name of the method is:
func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
print("Ranging")
}
In addition, a variable locationManager of the type CLLocationManager
needs to be created. The location manager is used to monitor and range the beacons. We add three methods to the view controller. Those methods are:
func getDVDBeaconRegion() -> CLBeaconRegion {
let beaconRegion = CLBeaconRegion.init(proximityUUID: UUID.init(uuidString: "E06F95E4-FCFC-42C6-B4F8-F6BAE87EA1A0")!, identifier:"nl.ximedes.myRegion1")
return beaconRegion
}
func getCashierBeaconRegion() -> CLBeaconRegion {
let beaconRegion = CLBeaconRegion.init(proximityUUID: UUID.init(uuidString: "E06F95E4-FCFC-42C6-B4F8-F6BAE87EA1A1")!,identifier: "nl.ximedes.myRegion2")
return beaconRegion
}
func startScanningForBeaconRegion(beaconRegion: CLBeaconRegion) {
locationManager.startMonitoring(for: beaconRegion)
locationManager.startRangingBeacons(in: beaconRegion)
}
The first two functions are similar; the only difference is that they use different proximity uuid’s. The proximity uuid is the uuid of one iBeacon. The last function tells the locationManager to monitor and to range the beacon regions.
At this point our view controller should contain the following the code:
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate, UITableViewDelegate, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
var inventar = Set()
var locationManager : CLLocationManager!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// beacon
func getBeaconRegion() -> CLBeaconRegion {
let beaconRegion = CLBeaconRegion.init(proximityUUID: UUID.init(uuidString: "E06F95E4-FCFC-42C6-B4F8-F6BAE87EA1A0")!, identifier: "nl.ximedes.myRegion")
return beaconRegion
}
func getCashierBeaconRegion() -> CLBeaconRegion {
let beaconRegion = CLBeaconRegion.init(proximityUUID: UUID.init(uuidString: "E06F95E4-FCFC-42C6-B4F8-F6BAE87EA1A1")!, identifier: "nl.ximedes.myRegion1")
return beaconRegion
}
func startScanningForBeaconRegion(beaconRegion: CLBeaconRegion) {
locationManager.startMonitoring(for: beaconRegion)
locationManager.startRangingBeacons(in: beaconRegion)
}
// Delegate Methods
func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
print("Ranging")
}
//tableview methods
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return inventar.count
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// cell selected code here
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "InventarCell", for: indexPath)
as! InventarCell
let product = inventar.first!
cell.inventar = product
return cell
}
}
Next, we will declare our location manager, the best place for doing so is in the viewDidLoad
function.
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager.init()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
startScanningForBeaconRegion(beaconRegion: getBeaconRegion())
startScanningForBeaconRegion(beaconRegion: getCashierBeaconRegion())
}
We could run the project and as soon we come close to one of our two beacons the console will print Ranging. So far so good! Let us extend our location manager method to support two different behaviors as soon we come close enough to an iBeacon.
func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
if beacons.count > 0 {
for tempBeacon in beacons {
if(String(describing: tempBeacon.proximityUUID) == "E06F95E4-FCFC-42C6-B4F8-F6BAE87EA1A0"){
if tempBeacon.proximity == CLProximity.immediate {
// add action here
}
}else if(String(describing: tempBeacon.proximityUUID) == "E06F95E4-FCFC-42C6-B4F8-F6BAE87EA1A1"){
if tempBeacon.proximity == CLProximity.immediate {
// add action here
}
}
}
}
}
So, what are we doing exactly? First, we check if our beacons array contains at least one beacon. If that is the case we go through the array and check if the proximity uuid is equal to the uuid of one of our iBeacons. If that condition is true the app checks if the beacon is close to the device. Pretty easy 😉
But we are still missing the implementation of the behaviour when an iBeacon is detected and the device is close enough to it.
if tempBeacon.proximity == CLProximity.immediate {
let cd = Inventar(title: "CD", amount: 1)
inventar.insert(cd)
self.tableView.reloadData()
}
if tempBeacon.proximity == CLProximity.immediate {
let dvd = Inventar(title: "DVD", amount: 1)
inventar.insert(dvd)
self.tableView.reloadData()
}
When we run the app now and we get close to one of our beacons with the uuid for example ABCDEFABC123456789
than a cd or a dvd will be added to our set and the table view is going to be refreshed.
By running the app now, we can see that a cd or a dvd will be added to our table view as soon the device is close enough to an iBeacon.
Conclusion
We have an app which can detect two different iBeacons and react with a different behaviour on them. When the app detects an iBeacon, a product is added to a table view.