Aug 07, 2017

Turning Data into Arrays in Swift

Update: I wrote this article for Swift 4. There is a newer version of this article for Swift 5.

Last time I talked about turning a Data object into a single scalar. Now I'm going to turn a Data object into an array of scalars.

Based on the last post, it's tempting to try something like this:

let data = Data(bytes: [0x01, 0x00, 0x00, 0x00,
                        0x02, 0x00, 0x00, 0x00])

let array = data.withUnsafeBytes {
                    (pointer: UnsafePointer<[Int8]>) -> [Int8] in
    return pointer.pointee // EXC_BAD_ACCESS
}

For that to work, the sequence of bytes that start at pointer must match the in memory representation of a Swift array containing those eight Int8 values. C arrays work like that. Swift arrays don't. Swift arrays are more complex than a sequence of bytes. If you tried the code above, you'd get a runtime crash.

Swift uses UnsafeBufferPointer to point to some sequence of bytes in memory. You can create an UnsafeBufferPointer from an UnsafePointer. Array has an initializer that takes such a buffer pointer a Sequence, and an UnsafeBufferPointer conforms to Sequence. Here's how to turn that data into an array of Int8 values.

So the plan is:

  1. Access an UnsafePointer with the method withUnsafeBytes
  2. Use the UnsafePointer to create an UnsafeBufferPointer
  3. Use the UnsafeBufferPointer to create an Array.

The type of the Array elements will be the type of the UnsafePointer in the closure given to withUnsafeBytes. Here's one way to do this to create an [Int8].

let data = Data(bytes: [0x01, 0x00, 0x00, 0x00,
                        0x02, 0x00, 0x00, 0x00])

let array = data.withUnsafeBytes {
                    (pointer: UnsafePointer<Int8>) -> [Int8] in
    let buffer = UnsafeBufferPointer(start: pointer,
                                     count: data.count)
    return Array<Int8>(buffer)
}

// array == [1, 0, 0, 0, 2, 0, 0, 0]

The code above interprets the data as 8 1-byte values. It's just as valid to interpret that data as 2 4-byte values, like below.

let data = Data(bytes: [0x01, 0x00, 0x00, 0x00,
                        0x02, 0x00, 0x00, 0x00])

let array = data.withUnsafeBytes {
                    (pointer: UnsafePointer<Int32>) -> [Int32] in
    let buffer = UnsafeBufferPointer(start: pointer,
                                     count: data.count / 4)
    return Array<Int32>(buffer)
}

// array == [1, 2]

As with [decoding numbers][1], ff you received data from a Bluetooth device with CoreBluetooth, there is no information to tell you how to decode the object. There's also no guarantee the order of the bytes will match the expected values in the Int32 example above. It's necessary to know ahead of time what the Data object represents.