27 June 2017 Philipp Darkow

Swift 3 iBeacon tutorial

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.