5 Best iOS BLE Frameworks and Libraries Comparison
Internet of things is one of the hottest topics nowadays and it's base is connection between devices. The main options currently are Wi-Fi and Bluetooth. In this article we'll take a look at some of the chosen open source iOS projects available on the market with a brief overview.
Example project description
It allows us:
- to choose between frameworks
- connect and disconnect from the device
- read and write values from a characteristic
Application has an easy interface for connection that every library's wrapper conforms to for easier comparison.
CoreBluetooth
Apple framework is available since iOS 5 SDK and every other tool from this list is based on it. It is using delegate pattern, every BLE specific event has it own method where you can handle it. Even if you will decide to use other framework, but you want to learn about working with BLE on iOS, I highly recommend reading Apple guide. It explains how BLE works under the hood and presents good practices with BLE programming that are tool independent, section is your way to go in each case. Here is implementation of CoreBluetooth:
func connect() {
central = CBCentralManager(delegate: self, queue: nil)
}
Firstly turning on central requires just to initialize CBCentralManager, actual connecting will take part later in delegate methods.
extension CBCentralController: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
guard central.state == .poweredOn else { return }
central.scanForPeripherals(withServices: [echoID])
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
central.stopScan()
central.connect(peripheral)
echoPeripheral = peripheral
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
peripheral.delegate = self
peripheral.discoverServices([echoID])
}
}
Next step is implementing the CBCentralManagerDelegate. Here we are scanning and connecting to peripheral and later discover services.
extension CBCentralController: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let service = peripheral.services?.first(where: { $0.uuid == echoID }) else { return }
peripheral.discoverCharacteristics([echoID], for: service)
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
guard let characteristic = service.characteristics?.first(where: { $0.uuid == echoID}) else { return }
echoPeripheral?.setNotifyValue(true, for: characteristic)
echoCharacteristic = characteristic
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
characteristicDidUpdateValue?(isReadingCharacteristicValue, characteristic.value)
isReadingCharacteristicValue = false
}
}
Lastly implementation of CBPeripheralDelegate. To finish connecting process we are discovering characteristic and setting notification for it. As we see we needed implementation of 6 delegate methods to accomplish this fairly simple task. Also more complicated problems will result in much more complicated code, which will be shattered between multiple methods and hard to manage.
func readValue() {
guard let characteristic = echoCharacteristic else { return }
echoPeripheral?.readValue(for: characteristic)
isReadingCharacteristicValue = true
}
func writeValue(_ value: Data) {
guard let characteristic = echoCharacteristic else { return }
echoPeripheral?.writeValue(value, for: characteristic, type: .withoutResponse)
}
After hustle with connecting, reading and writing to characteristic is very simple and straightforward.
BlueSwift
Our Netguru own framework BlueSwift is the newest library on the list. It's lightweight, basing only on CoreBluetooth and concentrated on reducing boilerplate code and ease of use. Here is implementation of BlueSwift:func connect() {
let echoIDString = "ec00"
echoCharacteristic = try! Characteristic(uuid: echoIDString, shouldObserveNotification: true)
echoCharacteristic.notifyHandler = { [weak self] data in
self?.characteristicDidUpdateValue?(false, data)
}
let echoService = try! Service(uuid: echoIDString, characteristics: [echoCharacteristic])
let configuration = try! Configuration(services: [echoService], advertisement: echoIDString)
echoPeripheral = Peripheral(configuration: configuration)
connection.connect(echoPeripheral!) { error in
print(error ?? "error connecting to peripheral")
}
}
That is the code for connecting to certain peripheral with services and characteristic specified before. The whole process of connecting was made into one method call, we just had to specify proper services and characteristics. Also we already implemented handling notifications.
func readValue() {
echoPeripheral?.read(echoCharacteristic) { [weak self] (data, error) in
guard error == nil else { return }
self?.characteristicDidUpdateValue?(true, data)
}
}
func writeValue(_ value: Data) {
echoPeripheral?.write(command: .data(value), characteristic: echoCharacteristic) { (error) in
print(error ?? "error writing characteristic value")
}
}
Writing and reading value from characteristic is very straightforward. This implementation certainly makes big impression as its very short and readable. Worth to note that you can still go through specific steps if needed. I don't like how initializers of Characteristic, Service and Configurations throws, cause of uuid validation. This string that would be passed as argument is almost always constant and even if it would be wrongly typed, it would be quickly spotted. Because of that we would have useless do-catch blocks, if we wouldn't force unwrap. The highest concern on this library is that it's not yet very popular and very new, so it may lack some functions i.e. writing without response.
BlueCap
The oldest 3rd party framework in this comparison was released in mid 2016. It is based on Swift implementation of Scala Futures. It's main goal was to avoid splitting code and deep nesting. Here is implementation of BlueCap:central = CentralManager()
let echoID = CBUUID(string: "ec00")
let discoveryFuture = central.whenStateChanges()
.flatMap { [weak central] state -> FutureStream in
guard let central = central, state == .poweredOn else { throw CentralError.notPoweredOn }
return central.startScanning(forServiceUUIDs: [echoID])
}.flatMap { [weak self] discoveredPeripheral -> FutureStream in
self?.central.stopScanning()
self?.peripheral = discoveredPeripheral
return discoveredPeripheral.connect(connectionTimeout: 10.0)
}.flatMap { [weak self] () -> Future in
guard let peripheral = self?.peripheral else {
throw CentralError.unlikely
}
return peripheral.discoverServices([echoID])
}.flatMap { [weak self] () -> Future in
guard
let peripheral = self?.peripheral,
let service = peripheral.services(withUUID: echoID)?.first
else {
throw CentralError.serviceNotFound
}
return service.discoverCharacteristics([echoID])
}
This part of code is responsible for turning on central, discovering peripheral and its services. Every step has it separate call to flatMap, that is later used further.
subscriptionFuture = discoveryFuture
.flatMap { [weak self] () -> Future in
guard
let self = self,
let peripheral = self.peripheral,
let service = peripheral.services(withUUID: echoID)?.first
else {
throw CentralError.serviceNotFound
}
guard let characteristic = service
.characteristics(withUUID: echoID)?
.first
else {
throw CentralError.dataCharactertisticNotFound
}
self.echoCharacteristic = characteristic
return self.echoCharacteristic!.startNotifying()
}.flatMap { [weak self] () -> FutureStream<Data?> in
guard let characteristic = self?.echoCharacteristic else {
throw CentralError.dataCharactertisticNotFound
}
return characteristic.receiveNotificationUpdates()
}
Now we are saving characteristic and setting notifications for it. Notifications are handled on didSet.
private var subscriptionFuture: FutureStream<Data?>? {
didSet {
subscriptionFuture?.onSuccess { data in
self.characteristicDidUpdateValue?(false, data)
}
}
}
We must retain the closure which will notify about changes in characteristic.
Writing and reading the characteristic is implemented like this:
func readValue() {
echoCharacteristic?.read().onSuccess { [weak self] in
guard let data = self?.echoCharacteristic?.dataValue else { return }
self?.characteristicDidUpdateValue?(true, data)
}
}
func writeValue(_ value: Data) {
_ = echoCharacteristic?.write(data: value, timeout: .infinity, type: .withoutResponse)
}
As we see implementation is fairly long. It succeed to keep related code together so we don't need any delegate implementation. Main problem with this framework for me is need for understanding Futures and its reliance on 3rd party library.
RxBluetoothKit
Next library is somewhat similar in spirit because it relies on framework that helps with asynchronous requests. But instead of Futures it is based on RxSwift, so it relies on Observables. Here is implementation of RxBluetoothKit:
func connect() {
let echoID = CBUUID(string: "ec00")
central = CentralManager()
subscriptionToCharacteristic = central
.observeState()
.startWith(central.state)
.filter { $0 == .poweredOn }
.flatMap { _ in self.central.scanForPeripherals(withServices: [echoID]) }
.take(1)
.flatMap { $0.peripheral.establishConnection() }
.flatMap { $0.discoverServices([echoID]) }
.flatMap { Observable.from($0) }
.flatMap { $0.discoverCharacteristics([echoID]) }
.subscribe { [weak self] characteristics in
self?.echoCharacteristic = characteristics.element?.first
self?.subscriptionToCharacteristic = self?.echoCharacteristic?
.observeValueUpdateAndSetNotification()
.subscribe {
self?.characteristicDidUpdateValue?(false, $0.element?.value)
return
}
}
}
This is implementation for connecting to peripheral, discovering its services, characteristics and setting notifications. As in previous implementation, every step takes place in flatMap call, but this time specific blocks are much shorter.
func readValue() {
_ = echoCharacteristic?
.readValue()
.asObservable()
.take(1)
.timeout(0.5, scheduler: MainScheduler.instance)
.subscribe {
self.characteristicDidUpdateValue?(true, $0.element?.value)
}
}
func writeValue(_ value: Data) {
echoCharacteristic?
.writeValue(value, type: .withoutResponse)
.subscribe()
.dispose()
}
Reading and writing is a little more complicated this time, but still relatively short and in spirit of RxSwift.
This time is implementation is really short and also we managed to connect and set notifications in single chain of asynchronous requests. The code isn't long and related code reside in the same method. Unfortunately reading and writing to characteristic is somewhat complicated and also using these framework effectively require knowledge of RxSwift.
Bluejay
Bluejay is nearly the same age as RxBluetoothKit, this library was released one month before. It's just basing on the CoreBluetooth and built-in swift features. It doesn't allow multiple connections, so you can't use this library if you want to talk with more peripherals at the same time. Here is implementation of Bluejay:
func connect() {
guard bluejay == nil else { throw CentralError.centralAlreadyOn }
bluejay = Bluejay()
bluejay.start(connectionObserver: self)
bluejay.scan(serviceIdentifiers: [echoServiceID], discovery: { (discovery, _) -> ScanAction in
return .connect(discovery, .seconds(0.3), .default, { (result) in
guard case .failure(let error) = result else { return }
print(error)
})
}) { (_, error) in
print(error ?? "Error scanning for peripherals")
}
}
Here is code for connecting to peripheral. Another very short and readable implementation. Also code look very swifty and this is big success as it was motivation for this lib.
func readValue() {
echoPeripheral?.read(from: echoCharacteristicID, completion: { [weak self] (result: ReadResult) in
switch result {
case .success(let data):
self?.characteristicDidUpdateValue?(true, data)
case .failure(let error):
print(error)
}
})
}
func writeValue(_ value: Data) {
echoPeripheral?.write(to: echoCharacteristicID, value: value, type: .withoutResponse) { result in
guard case .failure(let error) = result else { return }
print(error)
}
}
Reading and writing to characteristic are simple and use typical swifty callbacks.
extension BJCentralController: ConnectionObserver {
func connected(to peripheral: Peripheral) {
peripheral.listen(to: echoCharacteristicID) { [weak self] (result: ReadResult) in
switch result {
case .success(let data):
self?.characteristicDidUpdateValue?(false, data)
case .failure(let error):
print(error)
}
}
echoPeripheral = peripheral
}
}
Handling notifications require delegate implementation. Also when notifications are on reading from characteristic is unavailable.
As we can see this library heavily uses callbacks and we can even find there delegate, but it's okay for the library use-case, since you can connect only to one peripheral and that wouldn't mess up anything.
Summary
Summing up, we have had a look at the base of CoreBluetooth and 4 different 3rd parties, that helps us connecting BLE accessories. You should note, that I’ve only implemented central role without a thorough look at peripheral implementation.. Also in real life scenario if you have to implement a more complicated case, the problems of each library/framework will be more visible and troubling. What is the best choice, will always depend on the type of the project that you are working on. But for me if my use-case is rather simple and straightforward then I will go with pure CoreBluetooth, if on the other side I need to implement more complicated BLE scenario my first choice will be BlueSwift, as it allows working with BLE in very quick and elegant manner. If you already have RxSwift in your project and want to keep style consistent probably RxBluetoothKit will be also a good choice. Either way we have some decent tools for handling BLE connection on iOS. Interacting with Bluetooth on iOS has never been easier.
Photo by Bence Boros on Unsplash