In this entry, we’ll do a detailed code walkthrough of the changes that were made to get to this point.
WALKTHROUGH
The Starting Repo Tag
The Ending Repo Tag
The Comparison Between the Two Tags
The ITCB_SDK_Central_internal.swift
File
BEFORE:
public func sendQuestion(_ inQuestion: String) { self.question = inQuestion }
AFTER:
First, we added code to actually use Core Bluetooth to send the question to the Peripheral.
public func sendQuestion(_ inQuestion: String) { if let data = inQuestion.data(using: .utf8), let peripheral = _peerInstance as? CBPeripheral, let service = peripheral.services?[_static_ITCB_SDK_8BallServiceUUID.uuidString], let questionCharacteristic = service.characteristics?[_static_ITCB_SDK_8BallService_Question_UUID.uuidString], let answerCharacteristic = service.characteristics?[_static_ITCB_SDK_8BallService_Answer_UUID.uuidString] { peripheral.writeValue(data, for: questionCharacteristic, type: .withoutResponse) peripheral.setNotifyValue(true, for: answerCharacteristic) question = inQuestion } else { question = nil } }
That “self.
” was not necessary, so I removed it. Just a matter of style.
Next, we added code to the CBPeripheralDelegate
extension to catch it when the answer is returned.
public func peripheral(_ inPeripheral: CBPeripheral, didUpdateValueFor inCharacteristic: CBCharacteristic, error inError: Error?) { if let service = inPeripheral.services?[_static_ITCB_SDK_8BallServiceUUID.uuidString], let answerCharacteristic = service.characteristics?[_static_ITCB_SDK_8BallService_Answer_UUID.uuidString], let answerData = answerCharacteristic.value, let answerString = String(data: answerData, encoding: .utf8), !answerString.isEmpty { inPeripheral.setNotifyValue(false, for: answerCharacteristic) answer = answerString } }
I’ll walk through what I did, but first, I want to talk about a change in another file:
The ITCB_SDK_internal.swift
File
I added these two methods, which are Array
extensions:
extension Array where Element == CBService { public subscript(_ inUUIDString: String) -> Element! { return reduce(nil) { (current, nextItem) in return nil == current ? (nextItem.uuid.uuidString == inUUIDString ? nextItem : nil) : current } } } extension Array where Element == CBCharacteristic { public subscript(_ inUUIDString: String) -> Element! { return reduce(nil) { (current, nextItem) in return nil == current ? (nextItem.uuid.uuidString == inUUIDString ? nextItem : nil) : current } } }
I used Swift’s extension mechanism to add a “lookup” subscript to the Array class, if the contents are Service or Characteristic objects.
If they are, then I allow a String to be passed in, which is the String expression of the CBUUID that we have assigned a Service or Characteristic. This allows us to very quickly get a particular Service or Characteristic (because we are only using one Service -for now- we could just get the first element of the services Array, but this is a much better way to do it).
Back to the Peripheral Device Class
In this step, I simply extract the Data representation of the question String. The Characteristic write request wants Data, so we will need to provide Data.
let data = inQuestion.data(using: .utf8),
In this step, I simply cast our typeless peer property into a CBPeripheral.
let peripheral = _peerInstance as? CBPeripheral,
Using that Array extension I mentioned above, I get our “Magic 8-Ball” Service.
let service = peripheral.services?[_static_ITCB_SDK_8BallServiceUUID.uuidString],
Again, using the Array extension, I get the Question Characteristic from the Magic 8-Ball Service.
let charcteristic = service.characteristics?[_static_ITCB_SDK_8BallService_Question_UUID.uuidString]
First I ask the Peripheral object to request a write, using the Data from the String, and for the Question Characteristic. I am not looking for a response.
peripheral.writeValue(data, for: charcteristic, type: .withoutResponse)
Finally, I “subscribe” to the answer Characteristic, which tells Core Bluetooth to get back to me, when the answer Characteristic changes value.
peripheral.setNotifyValue(true, for: answerCharacteristic)
At this point, Core Bluetooth takes over, and will send the question to the Peripheral.
After that, I’ll set the “question” property to the String.
NOTE: The Central does not change the Characteristic. What it does, is send a new value that it is proposing for the Characteristic to the Peripheral. The Peripheral will then decide whether or not to accept the proposed change.
The ITCB_SDK_Peripheral_internal.swift
File (the Peripheral End of Things)
I added the CBPeripheralManagerDelegate
callback for receiving a write request.
public func peripheralManager(_ inPeripheralManager: CBPeripheralManager, didReceiveWrite inWriteRequests: [CBATTRequest]) { guard 1 == inWriteRequests.count, let mutableChar = inWriteRequests[0].characteristic as? CBMutableCharacteristic, let data = inWriteRequests[0].value, let stringVal = String(data: data, encoding: .utf8) else { return } mutableChar.value = data if nil == central { central = ITCB_SDK_Device_Central(inWriteRequests[0].central, owner: self) } _sendQuestionAskedToAllObservers(device: central, question: stringVal) }
Let’s walk through what happens:
- We use a guard statement to:
- Make sure that we only have one write request (We could, theoretically, get more than one)
- fetch the Characteristic that the Central wants to change, but as a mutable one (the one we created, earlier)
- Extract the proposed change, as Data
- Convert that data to a String.
- We assume the data is good, and simply change the Characteristic value to the new value.
- If the Central has not already been assigned, We create a new
ITCB_SDK_Device_Central
instance, with theCBCentral
that was passed in via the write request, and set it to our instance stored property. - We inform observers that the question was asked.
In order to accomplish step #3, I created a new initializer for the ITCB_SDK_Device_Central
class, with CBCentral
and “owner” arguments:
init(_ inCentral: CBCentral, owner inOwner: ITCB_SDK_Peripheral) { super.init() owner = inOwner _peerInstance = inCentral subscribedChar = nil }
I added a CBPeripheralManager
delegate callback to react to the Central subscription. I do the check for the central
property being nil
, because sometimes, the subscription may arrive before the write (remember what I said about “chaos”?), and we need to make sure the central is in place. We did the same check in the write request:
public func peripheralManager(_ inPeripheralManager: CBPeripheralManager, central inCentral: CBCentral, didSubscribeTo inCharacteristic: CBCharacteristic) { if nil == central { central = ITCB_SDK_Device_Central(inCentral, owner: self) } if let central = central as? ITCB_SDK_Device_Central { central.subscribedChar = inCharacteristic } }
WHERE WE ARE NOW
The “Magic 8-Ball” app is now completely functional. We can run a Mac or iPhone as a Peripheral (even multiple Peripherals), and any of the apps as a Central.
The Central will discover and list the Peripherals. It should be noted that a Central “captures” the Peripheral when it lists it, so no other Central will be able to find that Peripheral.
The Central can “ask a question” of a Peripheral.
The Peripheral will be notified that a “question has been asked,” and will allow the user to either send a random answer, or one selected from a list.
The Central will then display the answered question.
Now that we have that out of the way, let’s wrap up…