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. ↩