— Swift, React Native — 2 min read
While working on the library react-native-bluetooth-classic I got my first taste of Swift Streams through the ExternalAccessory framework. Since I've worked with streams in Java a number of times I thought this would be as straight forward, although not as straight forward, once I grasped the concept it finished itself pretty quickly.
Following the documentation on the framework, there are generally two methods for communicating with streams - which are described in the document Polling Versus Runloop, so I won't spend much time repeating the content:
As per the documentation this is not the preferred way of communicating with streams. Effectively, you just setup a thread loop that will continually ask if there is space/room and then write/read appropriately. I decided against this as:
The resources for Swift and Stream Delegate are pretty light. The only real documentation is for Objective C which just needed a little massaging into Swift. While going through this process I started with Swift over Objective C, this was mainly due to that I was more comfortable with the Swift syntax -
I've made a huge mistake.
I wish I had taken the time to power through Objective C off the bat, after getting my feet a little more wet it's not that bad and it just has too many benefits so far with React Native.
Within the StreamDelegate.stream
function we need to setup handling of all the available Stream.Event
values:
openCompleted
notifies us when the Stream has completed opening and the Input/Output streams are availablehasBytesAvailable
there are bytes on the InputStreamhasSpaceAvailable
there is room on the OutputStream for bytes. This is where there are some issues as this is only called AFTER bytes have already been sent.errorOccurred
if something goes downendEncountered
the stream was closed on either endHandling the Stream.Event.hasSpaceAvailable
, when space is available we want to call the writeData
:
1case .hasSpaceAvailable:2 // As per the documents, this event occurs repeatedly as long as you're writing data3 // in the examples I've found they just assume the initial write will work (using4 // a smaller value) and then continues on doing so.5 NSLog("Stream %@ has space available", aStream)6 writeData()7 break;
Once the delgate is setup, you can send some data - in this case though, sending data isn't just sending data. If you attempt to loop through the data and write to the stream, you'll get a bunch of funky threading issues and data errors in the buffer. Essentially what you need to do is:
writeData
to process the information1private func writeData() {2 if (outBuffer.isEmpty) {3 NSLog("(BluetoothDevice:writeData) No buffer data scheduled for deliver")4 return5 }6
7 let len:Int = (outBuffer.count > maxBytesPerSend) ? maxBytesPerSend : outBuffer.count8 NSLog("(BluetoothDevice:writeData) Attempting to send %d bytes to the device", len)9
10 let buffer:UnsafeMutablePointer<UInt8> = UnsafeMutablePointer.allocate(capacity: len)11 outBuffer.copyBytes(to: buffer, count: len)12 outBuffer.removeFirst(len)13
14 let bytesWritten = session?.outputStream!.write(buffer, maxLength: len) ?? 015 NSLog("(BluetoothDevice:writeData) Sent %d bytes to the device", bytesWritten)16}
This is where things go shady (in my mind) in that you can't just write to the stream. You can't just write it to the stream then remove it from the buffer (as when the write happens the delegate will be called) so you need to:
writeData
needs to pull out the data it's going to send and write it.When this happens you'll fall into a nice little loop of writing, ending gracefully and letting the stream delegate be called repeatedly until no more room is left. This works well for stopping as well, when the buffer is emptied and no data is written, it stops the hasSpaceAvailable
looping:
1func writeToDevice(_ message:String) {2 NSLog("(BluetoothDevice:writeToDevice) Writing %@ to device %@", message, accessory.serialNumber)3 if let sending = message.data(using: .utf8) {4 outBuffer.append(sending)5 }6
7 // If there is space available for writing then we want to kick off the process.8 // If all the data cannot be fully written, then the hasSpaceAvailable will be9 // fired and we can continue. In most cases, we shouldn't be sending that much10 // data.11 writeData()12}
I still don't think I'm managing the threads properly, but when I attempt to change the thread on which the delegate is run no data comes back. I'd love to hear if I'm missing something, this works, but it just doesn't feel right.
In order to manage reading from the stream we need to handle Stream.Event.hasBytesAvailable
1case .hasBytesAvailable:2 NSLog("Stream %@ has bytes available", aStream)3 readData()4 break;
where readData
pulls as many bytes as possible from the InputStream
. There aren't too many issues with this, as the Stream starts the process of reading (unlike the write method) and this will continue whenever new data is available.
1private func readData() {2 // Create the buffer that we'll read into3 let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxBytesPerReceive)4 let numBytesRead = session?.inputStream!.read(buffer, maxLength: maxBytesPerReceive) ?? 05
6 if (numBytesRead <= 0) {7 NSLog("(BluetoothDevice:readData) No buffer")8 return9 }10
11 // If there is a receiveDelegate then let them deal with the data and update with the remaining12 inBuffer.append(buffer, count: numBytesRead)13 if let bd = receivedDelegate {14 inBuffer = bd.onReceivedData(fromDevice: self, receivedData: inBuffer)15 }16}
All in all, this could probably be documented better - when I get a chance I'll come back and try to re-write it - but for now I'm hoping it will help someone (if not just me).