Swift – Bluetooth Low Energy communication using Flow Controllers

Posted on Posted in iOS

Introduction

Bluetooth Low Energy communication on iOS is not straight forward. You don’t just create a class and start reading/writing data like from local storage. Whole communication is based on asynchronous events and if you want to read data, you create a request with undetermined duration and wait for notification from iOS through delegate methods.

Recently, I’ve got inspiried by Flow Controllers/Coordinators and decided to apply this approach to handle flows specific for Bluetooth integration. Below I assume that you know already some BLE basics. For more information you may want to refer to: How to communicate with Bluetooth Low Energy devices on iOS.

Before you start communication

In order to start communication many conditions must be fulfilled and they are changing asynchronously.

  1. Bluetooth must be in “poweredOn” state.
  2. Peripheral must be discovered during scanning.
  3. Connection with Peripheral must be established.
  4. User has to confirm pairing by clicking “Pair” on system’s alert (only if pairing is required).
  5. Service with which you want to communicate must be discovered.
  6. Characteristic which you want to update or read data from must be discovered.

Therefore you must listen to multiple asynchronous events and control your current state in order to know when you can perform certain operations. It becomes difficult if you have to process many flows and can turn your code into spaghetti very fast.

Bluetooth flows

Usually mobile application providing BLE integration needs to handle:

  1. Pairing & bonding – to associate device with your application & phone and to establish the first connection.
  2. Synchronization – to exchange data, could be automatic or manually requested.
  3. Configuration – to allow modifications of peripheral’s settings.

Handling all states in one place – not good enough

Now if you create a single Bluetooth Service and start if-ing everything in delgate methods, code will become unreadable very fast. You could end up with something like this with tons of submethods:

Of course you can split this code into multiple methods, but trust me, it will be very hard to debug and read anyway. It’s also very likely that you will mismatch one flow with another accidentally.

In this example I’ve even made it simplier, I skipped checking which characteristic is updated etc. It gets even more complicated if you want to handle edge cases. Whole class will grow very fast and there is a risk that while changing from one flow to another, the old one will modify your class to unexpected state.

This approach may be good enough in some cases, however I would like to show also another way to solve this problem which is more scalable and will keep your code shorter while adding new features.

Solution – Flow Controllers

I decided to separate each flow while having only one Bluetooth Service, I also split parts of Bluetooth Service into extensions. After these operations I ended up with the following files:

  • BluetoothService:
    • BluetoothService.swift
      Only very basic operations – state, connect, disconnect, startScan, stopScan and property to assign FlowController.
    • BluetoothConnectionHandler.swift
      Extension to handle delegate methods related with connection like didConnect, didDisconnectPeripheral, didDiscover etc.
    • BluetoothEventsHandler.swift
      Extension to handle delegate methods related with Bluetooth events like didDiscoverServices, didUpdateValueFor etc.
    • BluetoothCommands.swift
      Extension to wrap preparing requests like binary payloads.
  • Flows implementation:
    • FlowController.swift
    • PairingFlow.swift
    • SynchronizationFlow.swift
    • ConfigurationFlow.swift

Bluetooth Service

  1. Service must inherit from NSObject in order to be able to assign itself as a delegate for CBCentralManager.
  2. To make this example easier, I assume that we will communicate only with one service and characteristic, it should be easy to extend it if you need. If you want to see iOS pairing alert, you need to request data from characteristic which is protected – requires encryption. Otherwise, you will just connect to peripheral without bonding.
  3. Here we will assign Flow Controller appropriate for current application state.
  4. You probably should request here for peripherals only with specific service to filter out devices not related with your application.
  5. Notifying Flow Controller about current state.

Bluetooth Connection Handler

  1. Additional filtering by name, you can skip this step or change logic to your own.
  2. Notifying Flow Controller about current state.

Bluetooth Events Handler

  1. Notifying Flow Controller about current state.

Bluetooth Commands

Flow Controller

The idea is to avoid in Flow Controller low level events like didDiscoverCharacteristicsFor and replace it with high level events like readyToWrite.

  1. There will be a circular reference between BluetoothService and FlowController, therefore this property has to be weak to avoid memory leak.

Pairing Flow

  1. Methods which will be triggered by View Controller.
  2. We implement timeout for pairing operation.
  3. FlowController’s methods receiving high level notifications from BluetoothService.
  4. When Bluetooth is in expected state, we can proceed with sending requests.
  5. Pairing on iOS is tricky, because system doesn’t inform an application in any way whether peripheral has been paired or not. The only way to check it is to verify if we can read from protected characteristic. You can read more in Swift – Bluetooth Low Energy – how to get paired devices?

Usage – pairing

Summary

Using approach described above we have:

  • Nicely separated modules with single resposibility.
  • BluetoothService and Bluetooth delegates don’t know anything about current operations and state, they only inform FlowController about events from Bluetooth module.
  • Flow implementations should not affect each other.
  • Once you change FlowController, you don’t have to worry that the old one will get some notifications.
  • Whole BluetoothService (and even PairingFlow) could be easily reused in another poject, because application-specific flows are loosely coupled and can be easily replaced or removed.
  • It’s easy to implement and integrate new flows.

I left SynchronizationFlow and ConfigurationFlow not implemented to make this article shorter, I think you should have a general idea now how to do it yourself. Leave a comment if you have some other ideas to improve this solution :).

You can find this implementation on GitHub: SwiftBluetooth.