Mar 20, 2017

Turning Data into Numbers in Swift

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 a generic UnsafePointer<T>, and you define what the type of data the pointer contains. If you know you were sent an a 32-bit integer, set your closure parameter as UnsafePointer<Int32>.

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
let number = data.withUnsafeBytes {
                            (pointer: UnsafePointer<Int32>) -> Int32 in
    return pointer.pointee
}
//number is an Int32

Why an Int32 instead of an Int? Because the size of an Int can change depending on what platform you're running on. The remote device is always going to send you a fixed-length number. If you know it's a 32-bit value, you should force the type to match that exactly.

It's also a good idea to make sure the length of the data matches the size of the type. That is, make sure the data has 32 bytes of data to read before reading the pointee property. Here's one way to do that.

let data:Data = characteristic.value //get a data object from the CBCharacteristic
let number: Int32 = data.withUnsafeBytes {
                            (pointer: UnsafePointer<Int32>) -> Int32? in
    if MemoryLayout<Int32>.size != data.count { return nil }
    return pointer.pointee
}

Take the unsafe in this method name seriously. It's possible to declare a closure that reads more bytes than you have. The compiler won't stop me from doing this, for example:

let data = Data(bytes: [0x00]) // one byte of data
let number: Int32 = data.withUnsafeBytes {
                            (pointer: UnsafePointer<Int32>) -> Int32 in
    return pointer.pointee // reading four bytes of data
}

The value of the number in that case is undefined, and will likely be different on successive runs of the program. Or maybe you'll get a crash.

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. If you need to change byte order, Core Foundation has utilities like CFSwapInt32BigToHost().