Bluetooth connection for iOS in Swift 3

There are a lot of tutorials about how to connect Bluetooth (Low Energy) device with iOS in the sea of Internet. So what will be different in this post? First of all, snippets of code seen here are written in Swift 3 and in the end I'll show you a hack on how to reconnect to your BLE device after iPhone reboots.

First we have to connect and communicate with BLE device so let's go through some code needed to do so.

Import

Unlike iBeacons, which use CoreLocation, to connect to a BLE device we'll use CoreBluetooth.

import CoreBluetooth

Delegates

To receive callbacks when communicating with the device will have to implement methods of two delegates: CBCentralManagerDelegate and CBPeripheralDelegate.

class BluetoothViewController: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate {
    //some code
}

Declaration of vars

Let's declare variables that we are going to use.

    var centralManager: CBCentralManager!
    var device: CBPeripheral!
    let DEVICE_NAME = "name"
    let DEVICE_SERVICE_UUID = "20510dfs1-3a03-0dab-b24f-07dfad6ef352"
    let DEVICE_CHARACTERISTIC_UUID = "00510001-3a03-0dab-b24f-07dfad6ef352"
    let RESTORE_IDENTIFIER = "restoreKeyForDevice"

We are going to use centralManager to scan, find, connect and manage BLE devices, while device will be used after connecting to go through characteristics of BLE device's services. To connect to a BLE service will need it's UUID, also we'll need an UUID for a specific characteristic. We'll use those two UUID's a lot in the code so it's better to make them a constant. We'll see the use of RESTORE_IDENTIFIER in the next section.

Instantiate centralManager

override func viewDidLoad() {  
  super.viewDidLoad()        
  centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey:RESTORE_IDENTIFIER])
}

Everything is pretty clear here except those options. Well, with options set like this we tell the system that after app is killed by the system, it should restore connection with specific identifier. Callback method for this will be explained later.

Start scanning for devices

When centralManager is done setting up it will call a function in which we are going to check if Bluetooth is on and then scan for peripherals.

func centralManagerDidUpdateState(_ central: CBCentralManager) {  
  if central.state == .poweredOn {
    central.scanForPeripherals(withServices: nil, options: nil)
  } else {
    print("Bluetooth not available.")
  }
}

Connecting

When centralManager discovers a device with the same name as DEVICE_NAME, it will connect to it.

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        if let peripheralName = peripheral.name, peripheralName == DEVICE_NAME {
            central.stopScan()
            self.device = peripheral
            self.device.delegate = self
            central.connect(peripheral, options: nil)
        }
   }

Discovering services

Once a connection is established we can discover device's services.

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    if peripheral == self.device {
        self.device.discoverServices(nil)
    }
}

Getting characteristics

Once we get a service with UUID equal to DEVICE_SERVICE_UUID we tell the device to discover charateristics.

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        for service in peripheral.services! {
            if service.uuid == DEVICE_SERVICE_UUID {
                peripheral.discoverCharacteristics(nil, for: service)
            }
        }
}

Subscribing to characteristic's value changes

So, when we identify characteristic with UUID equal to DEVICE_CHARACTERISTIC_UUID, we can subscribe to changes of it's value.

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {

        for characteristic in service.characteristics! {
            if characteristic.uuid == DEVICE_CHARACTERISTIC_UUID {
                peripheral.setNotifyValue(true, for: characteristic)
            }
        }
}

Receiving updates

After we subscribe for changes in characteristic's value, following delegate method will be called. For example, we can cast the data we received to String and print it.

 func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if characteristic.uuid == DEVICE_CHARACTERISTIC_UUID {
            if let data = characteristic.value, let command:String = String(data: data, encoding: .utf8) {
                print(command)
            }
        }
}

Restore state

Well most of the time we'll want to communicate with our device even if our app is in background. To do so you have to enable Uses Bluetooth LE accessories in Capabilities. After doing so our app will work in background and receive updates from device, right? Well, no. It will work most of the time, until our app is killed by the system because some other app needs memory or when iPhone reboots. Thankfully Apple has our first case covered. There is a delegate method that will be called when there is enough memory again and our device is advertising.

func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
           if let peripherals : NSArray = dict[CBCentralManagerOptionRestoreIdentifierKey] as? NSArray {
                for peripheral in peripherals {
                         if let peripheralName = peripheral.name, peripheralName == DEVICE_NAME {
                            central.stopScan()
                            centralManager = central
                            self.device = peripheral
                            self.device.delegate = self
                            central.connect(peripheral, options: nil)
                        }
                }
           }
 }

After this is called we have about 10 seconds to do something before our app becomes inactive.

As you can see this is only useful when our app is killed by the system. If iPhone is rebooted this delegate method will not be killed and to reconnect with our device, user has to manually restar the app. But, there is a small hack that we can use to launch our app in the background. We can use location manager to register for significant locaton updates. This way, our app will be launched in background about 3-5 minutes after iPhone reboots and we can check if we are connected there.

Well, that's all folks! Good luck with your projects and I hope that this short tutorial helps you.