Using Rx vs Delegates with CoreBluetooth
Reactive (or “Rx”) style programming is quickly becoming a popular design pattern in mobile development. What is it, and how does it compare to the tried-and-true delegation pattern? Should you be using Rx on your next mobile project? Follow along with me, and we’ll walk through a comparison of Rx vs. Delegates in the context of Bluetooth on iOS.
First, let’s review Delegation. If you’ve done some iOS development, you’re likely already very familiar with the delegation pattern. According to Wikipedia:
The delegation pattern is an object-oriented design pattern that allows object composition to achieve the same code reuse as inheritance.
In delegation, an object handles a request by delegating to a second object (the delegate).
What is Delegation?
Delegation is widely used across iOS in frameworks such as UIKit or CoreBluetooth. Let’s take a look at a delegation example from CoreBluetooth:
@objc class BluetoothManager: NSObject { public static let shared = BluetoothManager() ... override init() { super.init() centralManager = CBCentralManager(delegate: self, queue: nil, options: nil) ... }
When we initialize the CBCentralManager, we must pass in the “delegate” parameter. In this example, we’re passing “self” which refers to the BluetoothManager class.
CBCentralManagerDelegate
is a protocol defined in CoreBluetooth which includes a list of functions relevant to the behavior of a CBCentralManager
.
Further down in this BluetoothManager
file, we have an extension to the BluetoothManager
class where some methods from the CBCentralManagerDelegate
protocol have been implemented:
// MARK: - CBCentralManagerDelegate extension BluetoothManager: CBCentralManagerDelegate { func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { os_log(.info, log: BluetoothManager.ble_log, "peripheral: %@ \nadvertisementData: %@ \nrssi: %@", peripheral, advertisementData, RSSI) central.stopScan() connectingPeripheral = peripheral central.connect(peripheral, options: nil) } ... }
(Click here to see the full implementation using the CoreBluetooth delegation pattern)
Since our BluetoothManager
class was passed in as the delegate, those functions will be executed whenever they’re called on the delegate property that we passed in when initializing CBCentralManager
.
In the strictest sense of the term, “delegate” means a person sent or authorized to represent others, in particular an elected representative sent to a conference.
This definition also helps explain the term when used as a software design pattern. In our example, we’ve declared BluetoothManager
as the representative. The CBCentralManager
implementation can call on the delegate and allow it to respond to the methods defined in CBCentralManagerDelegate
.
This pattern is seen all over iOS development. Many components of UIKit rely on this pattern to implement functionality of components in an iOS app. Delegation is straightforward to understand and implement, and it’s relatively easy to test. Individual methods of a protocol can be unit tested, and mock objects can be created that conform to the protocol to assist with testing. There are many resources and tutorials on the internet that will use this pattern.
One of the primary negative aspects of delegation is that it can be a bit verbose. After you’ve implemented UITableViewDelegate
and UITableViewDatasource
for the hundredth time, you might be wondering if there is a better or more concise way to accomplish the same thing. While the use of extensions can help you organize your code, a class that implements multiple delegation protocols can end up being many lines of code. Navigating this can be fatiguing. You may find yourself jumping between different sections of the same file or between multiple files as you trace through the code. The bigger your project gets, the deeper this rabbit hole becomes.
What is Reactive (Rx)?
Now, let’s take a look at Reactive programming. Reactive is:
A declarative programming paradigm concerned with data streams and the propagation of change. (Wikipedia)
This pattern is made available to mobile platforms through the use of Reactive Extensions:
Reactive Extensions (aka ReactiveX) is a set of tools allowing imperative programming languages to operate on sequences of data regardless of whether the data is synchronous or asynchronous (Wikipedia)
Reactive Extension implementations are available for many languages in platforms such as RxSwift, RxJava, RxPy, RxJS, and more.
The basic concept of Reactive programming is that it allows you to write code that responds to a stream of events. This model lends itself well to asynchronous things such as network or Bluetooth activity.
An Observable is an object that represents an event stream. The events are of a defined type such as Observable<Int>
, Observable<String>
, or it could be a more complex type such as Observable<CBPeripheral>
. In addition to emitting events, an Observable can terminate with an error, or with completion. At that point, the Observable can no longer emit anything. An Observer is the other side of the coin — a subscription to an Observable that listens for the events that are emitted over time. The Observer is notified when an item is emitted, if an error terminates the stream, or if the stream comes to completion.
Rx programming is a deep subject, and a full dive into it is beyond the scope of this blog post. I’m going to focus on a specific library for bluetooth called RxBluetoothKit and how it compares to the standard CoreBluetooth implementation that uses delegation. If you’d like to read more about Reactive programming, here are some resources for that:
Learn & Master ⚔️ the Basics of RxSwift in 10 Minutes
Getting Started With RxSwift and RxCocoa
Understanding the RxBluetoothKit Library
RxBluetoothKit is an Rx library for CoreBluetooth from Polidea. It’s an extension to RxSwift that wraps the CoreBluetooth functionality in the Rx-style Reactive pattern. This allows you to implement Bluetooth functionality that allows you to write code that reacts to peripheral discovery, service discovery, or characteristic discovery as streams of events. The reactive pattern works really well for these asynchronous tasks, and ultimately you can accomplish the equivalent functionality of the CoreBluetooth delegation pattern but with significantly less lines of code.
NOTE: If you’re working on an Android project, check out RxAndroidBle.
Here’s an example that uses RxBluetoothKit to connect to a peripheral and read a value from a characteristic:
centralManager.scanForPeripherals(withServices: [serviceUUID]) .take(1) .flatMap { $0.peripheral.establishConnection() } .flatMap { $0.discoverServices([self.serviceUUID]) } .flatMap { Observable.from($0) } .flatMap { $0.discoverCharacteristics([self.characteristicUUID]) } .flatMap { Observable.from($0) } .flatMap { $0.readValue() } .subscribe(onNext: { if let value = $0.value { var number: UInt8 = 0 value.copyBytes(to:&number, count: MemoryLayout<UInt8>.size) os_log(.info, log: RxBluetoothManager.rxble_log, "Value read: %d", number) } else { os_log(.error, log: RxBluetoothManager.rxble_log, "Error reading value") } }) .disposed(by: disposeBag)
This code starts by scanning for peripherals that advertise the service UUID we’re looking for. Then, it takes only the first item (a peripheral) emitted from that stream and establishes a connection. From there, it discovers its services, as well as the characteristic we are looking for, and finally reads a value from that characteristic. When the read value is emitted, we convert it to a UInt8
and print it to the log. This is accomplishing a lot in very few lines of code!
Rx vs Delegates
Click here to see the full comparison of code for reading a value from a characteristic using each pattern.
If we compare the code needed to accomplish the same task in the standard CoreBluetooth delegation pattern, the Rx pattern code is about half as many lines of code. That’s pretty significant. The Rx code is really concise, and if you understand the pattern, you can quickly see what it’s accomplishing without jumping all over the file. With delegation, in order to follow the flow of code, you may have to jump around between an extension where you had implemented the delegate protocol at the bottom of the file, back up to a function from the class at the top, or to other files. This can feel a little chaotic.
However, while the delegation pattern is a bit more verbose, it’s more friendly to someone reading the code for the first time. With delegation, the method names and return types are very clear. With Rx, it can be a little difficult to keep track of what’s being returned.
From the Rx example above, that stack of .flatMap and $0 usages are not super descriptive at a glance. Unfortunately, it can also be a bit more difficult to debug issues in the Rx code as well. Sometimes the error messages aren’t as clear, or an issue further up has cascaded into a confusing error message further down the function chain.
When it comes to unit testing your code, there may be some added complexity to writing tests for Rx style code. You may need to become familiar with additional packages or extensions that help facilitate scripting or simulating emitted events that you wish to test.
A final note on Rx: it is a 3rd party library. In some cases, this is reason enough not to choose it for your project. Although RxSwift and RxBluetoothKit are well maintained, you may be at the mercy of the teams that manage these frameworks next time there is an iOS update that causes an issue. Apple does have their own implementation of a reactive-style framework called Combine. I’ll discuss that a bit more towards the end of this post.
Which Should You Choose?
There are valid reasons to choose either pattern over the other. One of the most important things to consider when choosing which pattern to use for your next project is what the rest of your team is comfortable with. With Rx, the biggest hurdle is generally the learning curve. If you’re trying to jump into it and no one on your team has Rx experience, just make sure you adjust your estimate and timeline to account for some additional ramp up time. If you’re dealing with a tight timeline or smaller budget, maybe sticking with something your team is comfortable with is the best choice.
Another factor to consider is whether you are comfortable adding 3rd party libraries to your project. If you’re looking to keep the project light, sticking with CoreBluetooth delegation might be your best choice. If you’re already using RxSwift in your project, and you need to add some BLE functionality, then I’d certainly recommend using RxBluetoothKit.
Ultimately, I recommend giving RxBluetoothKit a try if you have the opportunity. The Rx pattern is quickly becoming very popular, so it would be valuable to get familiar with it sooner rather than later. It also fits very well with the asynchronous nature of BLE functionality. While there is a bit of a learning curve, once you get the hang of it you may find that you really appreciate the amount you can accomplish with such concise code.
CoreBluetooth Delegation | RxBluetoothKit |
Pros: – Fairly Straightforward / shorter learning curve – Familiar pattern – No 3rd party libraries – Easier to debug Cons: – A lot of “boilerplate” even for simple functionality – Verbose functions | Pros: – Very concise – Natural fit for projects using RxSwift – Pattern fits well for asynchronous code Cons: – Steeper learning curve – Requires 3rd party libraries – A little more difficult to write tests |
A Note about Combine
Combine is a framework Apple introduced with iOS 13 that is essentially its 1st-party solution for reactive style programming. It’s still very new, and may continue to undergo changes as many new Apple frameworks do. While Apple hasn’t directly added Combine functionality to CoreBluetooth, I did come across a very interesting GitHub project that does exactly that:
https://github.com/vukrado/CombineBluetooth
This project is pretty new, and using Combine means it’ll only support iOS 13 and above. But the idea of being able to use reactive style programming for Bluetooth functionality without any 3rd party libraries sounds pretty great!
As Apple adopts more modern development practices into the iOS ecosystem, I think there’s a chance that some Combine CoreBluetooth functionality could be introduced into iOS.
Interested in Learning More?
Other articles written by Julian:
Mobile Developer’s Advice for Implementing Device Firmware Updates
How to Use Apple’s Swift Package Manager
How to Use Node.js to Speed Up BLE App Development