We are now ready to start actually implementing the Bluetooth code.
Apple’s Core Bluetooth SDK uses the concept of “Managers” to implement Bluetooth capabilities for Central and Peripheral devices.
If we will be running the device as a Bluetooth Central, then we will instantiate CBCentralManager
. If we will be running as a Peripheral, then we instantiate CBPeripheralManager
. An instance of one these will be associated with each of the SDK instances that we create, and we will be managing these “under the radar.” The user of the SDK will not have any idea these are being used.
Picking A Mode
If we are running the Mac or iOS version of the app, in the initial screen, we are asked to pick a mode: either Central, or Peripheral. Once we do this, then an instance of the SDK is created that implements that mode.
At the time the SDK is created, we also create a Manager instance for that SDK, and start it “doing what it does.” That means that Central devices will start scanning for Peripherals, and Peripheral devices will start advertising the “Magic 8-Ball” Service.
WHAT’S CHANGED
The Current Repo Tag
This tag represents the end result of the code changes that will be discussed in this entry. If we want to sync to the starting point, then we should check out this tag.
This is a comparison between the two tags. A lot has happened.
Let’s walk through the changes
We’ll walk through each file’s changes between the two tags:
The ITCB_SDK.swift File
- I now derive the base class from
NSObject
.
This allows us to use the derived classes as delegates for the Core Bluetooth Managers (delegates need to be NSObjectProtocol-based. The best way to conform to this –rather extensive– protocol is to derive fromNSObject
).
In order to make this work, I also needed to add an empty-argument initializer that calls the “super” init. - I added the
ITCB_SDK._managerInstance
stored property.
It is also declared @objc dynamic, so it can be easily overridden by subclasses.
This is the place that we store the actual Core Bluetooth Manager instance that we will be creating.
The ITCB_SDK_Internal.swift File
- I now import the Core Bluetooth Module.
It is important that I keep the CoreBluetooth dependency out of the public interface, so this only happens in the internal part of the SDK. - I added static internal declarations of our new CBUUIDs.
- I implement the new
_managerInstance
stored property.
I added a simple cast, and a stored property, where subclasses will store their specific instance. - I filled in the code for the
_isCoreBluetoothPoweredOn
Bool.
Now that we have a manager, I can query it for the state of the Bluetooth subsystem. - I added a
_peerInstance
stored property to the base device class.
This allows us to associate a Central or Peripheral instance with a device. - I removed the code for the Central and Peripheral subclasses.
This code went into two files that I “spun off” from the original aggregate file.
The ITCB_SDK_Central_internal.swift and ITCB_SDK_Peripheral_internal.swift Files
These are the two files that I “spun off” from the original ITCB_SDK_Internal.swift file. Since we will be adding a lot of mode-specific implementation, I thought it prudent to make these separate files.
They are not quite “one-class” files, though. Each file contains the SDK and Device class for the particular mode. These derive from the base classes in the ITCB_SDK_Internal.swift file.
In each, I added code to instantiate the particular manager object that applies:
override var _managerInstance: Any! { get { if nil == super._managerInstance { super._managerInstance = CBCentralManager(delegate: self, queue: nil) } return super._managerInstance } set { super._managerInstance = newValue } }
Note the way that I apply the delegate after the fact. This will also work:
override var _managerInstance: Any! { get { if nil == super._managerInstance { super._managerInstance = CBPeripheralManager() peripheralManagerInstance.delegate = self } return super._managerInstance } set { super._managerInstance = newValue } }
ITCB_SDK_Central_internal.swift
I also added code to conform the specialization to CBCentralManagerDelegate
:
Swift style prefers us to use extensions as a way to add protocol conformance to a class, struct or enum:
extension ITCB_SDK_Central: CBCentralManagerDelegate {
I added a handler for the required CBCentralManagerDelegate.centralManagerDidUpdateState
method. I use this to start scanning for peripherals, as soon as the Bluetooth system reports that it is powered on.
NOTE: We should not start scanning (or advertising) right after instantiating the manager class. Instead, we should register as delegates, and start scanning or advertising when the manager indicates that it is ready (.
poweredOn
).
public func centralManagerDidUpdateState(_ inCentral: CBCentralManager) { if .poweredOn == inCentral.state { inCentral.scanForPeripherals(withServices: [_static_ITCB_SDK_8BallServiceUUID], options: [:]) } }
I implement the CBCentralManagerDelegate.centralManager(_:, didDiscover:, advertisementData:, rssi:)
method. Right now, all it does is print a report. That will change.
public func centralManager(_ inCentral: CBCentralManager, didDiscover inPeripheral: CBPeripheral, advertisementData inAdvertisementData: [String : Any], rssi inRSSI: NSNumber) { if let peripheralName = inPeripheral.name { print("A Peripheral Magic 8-Ball, named \"\(peripheralName),\" was discovered, at a signal strength of \(inRSSI), and with this advertisement Data: \(String(describing: inAdvertisementData)).") } }
NOTE: I filter for a valid device name. Even though it is not mentioned anywhere in the documentation, it appears as if we can get discovery callbacks when the device is “not quite ready.” I have found that filtering for the name will avoid “false positives.”
ITCB_SDK_Peripheral_internal.swift
In this file, I added conformance to the CBPeripheralManagerDelegate
protocol:
extension ITCB_SDK_Peripheral: CBPeripheralManagerDelegate { public func peripheralManagerDidUpdateState(_ inPeripheral: CBPeripheralManager) { if .poweredOn == inPeripheral.state { inPeripheral.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [_static_ITCB_SDK_8BallServiceUUID], CBAdvertisementDataLocalNameKey: localName ]) } } }
In the required handler, I start advertising as soon as the Bluetooth system reports that it is powered on.
Note that I send the CBUUID of the “Magic 8-Ball” Service, and the data is our device name. It’s a good idea to keep advertising data as terse as possible. It is not the place to recount our life story.
WHERE WE ARE NOW
At this point, the apps still behave exactly as they did before.
However, they are now instantiating the Core Bluetooth toolkit. Peripherals are advertising our Service, and Centrals are scanning for Peripherals that advertise our Service.
If you run an instance on the Mac (using Xcode), and select Central as the mode, then fire it up on a device, and run Peripheral as the mode, you will see some echoes in the debugger console that look like this:
A Peripheral Magic 8-Ball, named "iPhone Xs Max," was discovered, at a signal strength of -32, and with this advertisement Data: ["kCBAdvDataIsConnectable": 0, "kCBAdvDataChannel": 37, "kCBAdvDataLocalName": iPhone Xs Max].
That means that Bluetooth is working on both devices, and that the Central was able to “find” the Peripheral (The above example was generated by using Peripheral mode on an iPhone).
More than one Peripheral can be discovered.
This particular discovery happened in a very “noisy” environment, with many BLE devices advertising their Services. By filtering for a Service, we can ignore the carnival barkers, and just listen for the one we want.
This Must Happen On Devices
Again, Bluetooth won’t work in simulators. It must run on a device. The only device that we can run as before is the Mac, as there is no simulator. We are probably best served running the Peripheral on an iOS device (iPhone, iPad or iPod Touch), and the Central on our Mac.
In any case, if you run it via the debugger once, on a device, the app will be installed, and you’ll be able to run it again, without having to use the debugger.
If the device is run in Peripheral mode, you’ll see the discovery results in the debug log, if you run the Mac version from Xcode, in Central mode.