When you read from a Bluetooth Device, Core Bluetooth returns a Data object with the contents. A Data object is basically a container for raw bytes without a type. How do you get that data into a proper Swift type like an Int or a Float? Swift’s type system is strict enough that you can’t force a typecast to an unrelated type. You can’t cast the bytes contained in a Data object to an Int. There is no Int constructor that takes a Data or raw bytes. The method withUnsafeBytes is the method you need for all your data conversion needs.
Before you start, you have to know what the data actually is. There is no way of knowing how to interpret the data without knowledge of the device that’s sending you that data. Hopefully there is some documentation to refer to.
The method withUnsafeBytes take a closure, and it passes a pointer into that closure. The type of the pointer is an UnsafeRawBufferPointer.1 The body of the closure should create a new variable using the contents of that pointer.
The body of that closure is your opportunity to move the data into your desired type.
In this case my desired type is an Int32. I can access the Int32 value of the pointer through the pointee property.
The return type of the closure is also generic, and the return type I declare is the return type of the function itself. If I return an Int32 from the closure, the method withUnsafeBytes returns an Int32.
Here’s how it looks. Pretty short and easy once you have all the pieces straight.
let data:Data = characteristic.value //get a data object from the CBCharacteristic // with type annotations let number = data.withUnsafeBytes( {(pointer: UnsafeRawBufferPointer) -> Int32 in return pointer.load(as: Int32.self) }) // same method call, without type annotations let number = data.withUnsafeBytes { pointer in return pointer.load(as: Int32.self) }
UnsafeRawBuferPointer is a Collection of UInt8. Collections know their own length, so it can prevent you from loading a type that needs more memory than you have. For example, the following is a runtime error:
let data = Data([0x01]) // one byte let array0 = data.withUnsafeBytes { pointer in return pointer.load(as: Int32.self) //four byte type } // Fatal error: UnsafeRawBufferPointer.load out of bounds
One final warning. I mentioned that you can’t know the data type without some documentation. Even if you know that the number is a 32-bit integer, you also have to know what the byte order of the number is. Your Bluetooth device could be sending a big-endian number to your little-endian iPhone. Int has byte-order methods init(bigEndian: Int), and var bigEndian: Int if you need to swap byte order.
-
The closure used to take a generic
UnsafePointer<T>, but that version of the method is deprecated is Swift 5. ↩