Scan and broadcast to nearby devices with Core Bluetooth
The topic of this post is about Bluetooth and how we can use it in our applications.
Bluetooth technology can be used in a variety of ways, and in this post I am going to focus on how we can make our device broadcast its presence to nearby devices, and also how we can scan for nearby devices that are running our app.
In order to implement this app, we are going to use Core Bluetooth
, a framework to communicate between BLE devices provided by Apple.
Long story short, in this article, I am going to create a simple app to demonstrate how to use Core Bluetooth
to implement the above mentioned scenario. The app will consists of a UITextField
, where the user will enter the name for the device and two instances of UIButton
. The one button will act as a trigger to start broadcasting, while the other will start the process of scanning other nearby devices.
Implementation
Before we dig deeper to the implementation, let’s take a look on some terminology. A BLE(Bluetooth Low Energy) device can be categorized into two roles: peripheral
and central
. A device acting as a peripheral advertises its presence, whereas a device acting as a central listens to these advertising packets. So in our case, we want our application to act both as a peripheral and a central.
With separation of concerns in mind, we are going to create a class that will handle the logic of broadcasting and scanning for devices using CoreBluetooth
, and then we will see how we can use this class in a Catalyst application.
Before we jump to the actual implementation, let’s take some time to think about the desired behavior of this class and define the corresponding protocol
.
First of all, we are going to need two functions that will be responsible for the initialization of the advertising and the scanning process. For the advertising one, we need to pass a String
as a parameter, which is going to be used as the name of our device. Then, we have to somehow notify the uses of this class when a new device is discover. Maybe for this, we can use a delegate with a function that will be called once a new device is found and a hash map to store the peripherals.
protocol BluetoothManagerDelegate: AnyObject {
func peripheralsDidUpdate()
}
protocol BluetoothManager {
var peripherals: Dictionary<UUID, CBPeripheral> { get }
var delegate: BluetoothManagerDelegate? { get set }
func startAdvertising(with name: String)
func startScanning()
}
Now that we have our protocol ready, we can move on to the actual implementation. Let’s create a new class named CoreBluetoothManager
. This class will conform to the BluetoothManager
protocol and will have the following content:
class CoreBluetoothManager: NSObject, BluetoothManager {
// MARK: - Public properties
weak var delegate: BluetoothManagerDelegate?
private(set) var peripherals = Dictionary<UUID, CBPeripheral>() {
didSet {
delegate?.peripheralsDidUpdate()
}
}
// MARK: - Public methods
func startAdvertising(with name: String) {
self.name = name
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
}
func startScanning() {
centralManager = CBCentralManager(delegate: self, queue: nil)
}
// MARK: - Private properties
private var peripheralManager: CBPeripheralManager?
private var centralManager: CBCentralManager?
private var name: String?
}
Please mind that you have to add
import CoreBluetooth
in the beginning of the file.
First, we describe the public properties for the delegate
and the peripherals
. When the value of the peripherals
is updated, we call the peripheralsDidUpdate
function to notify the delegate.
Then, we define the implementation for the func startAdvertising(with name: String)
requirement. In this function, we initialize an instance of CBPeripheralManager
which we will store in a private property. CBPeripheralManager
is part of the CoreBluetooth
framework and its primary function is to allow us to advertise to other devices.
For now the compiler will complain because we have set
self
as the delegate and it is not conforming to theCBPeripheralManagerDelegate
protocol. We are going to fix this soon.
Moving on, we have the implementation for the startScanning
requirement. In this case we initialize an instance of CBCentralManager
which we also store in a private property. CBCentralManager
is responsible for scanning for nearby BLE devices.
Since the compiler is complaining about the delegate, let’s try to implement them. First, let’s add an extension to the CoreBluetoothManager
that will conform to the CBPeripheralManagerDelegate
protocol and add the following content:
extension CoreBluetoothManager: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
if peripheral.state == .poweredOn {
if peripheral.isAdvertising {
peripheral.stopAdvertising()
}
let uuid = CBUUID(string: Constants.SERVICE_UUID.rawValue)
var advertisingData: [String : Any] = [
CBAdvertisementDataServiceUUIDsKey: [uuid]
]
if let name = self.name {
advertisingData[CBAdvertisementDataLocalNameKey] = name
}
self.peripheralManager?.startAdvertising(advertisingData)
} else {
#warning("handle other states")
}
}
}
Here, we provide an implementation for the peripheralManagerDidUpdateState
requirement. In the body of this function we check if the peripheral.state
is .poweredOn
as described on Apple’s documentation.
Before you call CBPeripheralManager methods, the peripheral manager object must be in the powered-on state, as indicated by the
CBPeripheralManagerState.poweredOn
. This state indicates that the device (your iPhone or iPad, for instance) supports Bluetooth low energy and that its Bluetooth is on and available for use.
After that, we check if the peripheral is already advertising and if it does, we stop it so that we can start advertising again with the new advertisingData
which we are doing exactly after. advertisingData
is a dictionary that contains the data to advertise. The supported advertising data types are CBAdvertisementDataServiceUUIDsKey
and CBAdvertisementDataLocalNameKey
. The first one is a UUID specific to our application and it will be used to filter out other BLE devices, whereas the second one is the local name of the peripheral.
For the service UUID, we add an enum
named Constants
, where we add a case for the SERVICE_UUID
, like in the following snippet
enum Constants: String {
case SERVICE_UUID = "4DF91029-B356-463E-9F48-BAB077BF3EF5"
}
Finally, we can call the startAdvertising
function to initiate the advertising process.
Let’s move now to the scanning part of our class. We are going to add another extension to CoreBluetoothManager
and this time it will conform to the CBCentralManagerDelegate
protocol. The content of this extension will be the following:
extension CoreBluetoothManager: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
if central.isScanning {
central.stopScan()
}
let uuid = CBUUID(string: Constants.SERVICE_UUID.rawValue)
central.scanForPeripherals(withServices: [uuid])
} else {
#warning("Error handling")
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
peripherals[peripheral.identifier] = peripheral
}
}
In the same fashion as with the CBPeripheralManagerDelegate
extension, we satisfy the centralManagerDidUpdateState
requirement for the CBCentralManager
this time, and again we are checking if the central.state
is .poweredOn
and if it is already scanning then we call the stopScan
. Finally, we call scanForPeripherals
and pass the CBUUID
with the SERVICE_UUID
that we are using for our application. This way, we are going to scan for devices that advertise this service and ignore all the others.
After starting the scanning process, we have to get informed when a device is discovered and for this reason, we add an implementation for the didDiscover
requirement of the CBCentralManagerDelegate
. There, we just update the peripherals dictionary with the peripheral that we have just discovered.
And that’s it regarding the BluetoothManager
. Let’s now move on and see how we can use this class.
Application
As mentioned in the introduction, the application will have one screen which will contain a UITextField
to enter the device name that we will use when calling the startAdvertising
, and two instances of UIButton
, one for triggering the advertising process and one for triggering the scanning process.
Since the post is about Core Bluetooth
I will focus on how to use the class CoreBluetoothManager
that we have just created and skip the UI part. The UI is created with UIKit, AutoLayout and programmatic views. Furthermore, the whole code can be found on GitHub and it’s a Catalyst application, meaning you can run it on iPhone, iPad or Mac.
To use the CoreBluetoothManager
, we instantiate an instance of it in the SceneDelegate
and we inject it to our only UIViewController
, the BluetoothViewController
.
// SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
private lazy var bluetoothManager = CoreBluetoothManager()
.
.
.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
.
.
.
let vc = BluetoothViewController(bluetoothManager: bluetoothManager)
.
.
.
}
}
In the BluetoothViewController
, we receive an instance of a class that conforms to the protocol BluetoothManager
in the init function and we store it in a private property.
Then, we provide the implementations for the two buttons. For the first one, we use the bluetoothManager
to call the startAdvertising
and we pass the value of the UITextField
as the device name. For the second button, we set the delegate
of the bluetoothManager
to self
and call the startScanning
function to trigger the scanning process.
As a result, we have to make BluetoothViewController
conform to the protocol BluetoothManagerDelegate
, by adding an extension and providing an implementation of the method peripheralsDidUpdate
to fulfill BluetoothManagerDelegate
’s requirement. In this function, we just print the names of the peripherals on the console, but we could potentially reload an instance of a UITableView
that would present the nearby devices.
// BluetoothViewController.swift
class BluetoothViewController: UIViewController {
init(bluetoothManager: BluetoothManager) {
self.bluetoothManager = bluetoothManager
super.init(nibName: nil, bundle: nil)
}
.
.
.
private var bluetoothManager: BluetoothManager
@objc private func startAdvertising(sender: UIButton!) {
nameTextField.resignFirstResponder()
.
.
.
bluetoothManager.startAdvertising(with: name)
}
@objc private func startScanning(sender: UIButton) {
bluetoothManager.delegate = self
bluetoothManager.startScanning()
}
}
extension BluetoothViewController: BluetoothManagerDelegate {
func peripheralsDidUpdate() {
print(bluetoothManager.peripherals.mapValues{$0.name})
}
}
And that’s about it for the application code. But before we run the application, we have to make some further adjustments, to enable the usage of Bluetooth.
First, we have to add an entry for the key NSBluetoothAlwaysUsageDescription
to our Info.plist
file, like in the following snippet
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Our app uses Bluetooth to find other devices</string>
Then, we have to select the target of the app, go to Signing & Capabilities
> App Sandbox
and enable the Bluetooth
checkbox.
Lastly, make sure to enable the Bluetooth on the device that you are running the application.
Now our app is ready!! Feel free to run it and start advertising and scanning for nearby devices!
Conclusion
In this post, we have seen how to use the CoreBluetooth
framework to advertise that an application is running on a device and also scan for nearby devices that are running the application. These concepts can be used as a base and build more complex application that will rely on Bluetooth and the communication of nearby devices, like for example a chat app, or maybe an AirDrop-like solution to share documents with other non-Apple devices.
Thanks for reading, I hope you find this post useful! Feel free to find me on Twitter and share your comments about this post!