Monday, January 9, 2017

How to use Swift's 'Data' structure to carry different types of data



Introduction


Swift provides the Data structure as an easy way to deal with byte buffers (sometimes called byte arrays). this means it holds raw bytes. This allows for very fast transfer of data (like over bluetooth).

It may be a little tricky to use it with some data types. This post provides ways to use Data to hold some of the most useful data types.

Just keep in mind that there has to be some kind of an "agreement" between your code and the consuming party of this data (it should know what data type and size are being sent).

If you are using a Swift playground, just import CoreData before you begin.

Code


UInt8 (and maybe booleans)

This is the primitive byte type as mentioned in the Swift docs; it is a raw byte after all 😅. It is the most straightforward to use as well.

Data can be used to hold an array of UInt8s directly.

let data = Data([1, 5])

/* or maybe create a flag,
 * change it in different places of the app
 * and send it like so..
 */
var flag: UInt8 = 0
let data = Data([flag])

This can also be used as a boolean flag by using 0 and 1 as values for a single element array (like you've seen in the above example).

String

Strings are also pretty easy to deal with.

let myString = "Cats are awesome"
let data = string.data(using: .utf8)

Just note that depending on your encoding of choice, the size is defferent. For this example with utf8, every character is 1 byte.

Float, Double and other number types

Other number types can take advantage of the flexibility of the UnsafeRawPointer to point directly to the memory location of the variable holding the data you want to send. This pointer is then used to initialize the Data object.

var myDouble = -9.52

let data = withUnsafePointer(to: &myDouble) {
    Data(bytes: $0, count: MemoryLayout.size(ofValue: myDouble))
}

This creates a Data object with the first parameter being a pointer to the myDouble variable.

The $0 is Swift's shorthand name of the UnsafePointer of myDouble. Check this answer if you need more info.

The second argument is the length which the Data initializer needs to walk through the memory to represent your variable, for this we have used the MemoryLayout.size(ofValue:) function to get the size of the myDouble variable automatically (which we know is 8 bytes, or 64 bits).

So this basically says: Starting at the position of myDouble in memory, walk 8 bytes, copy these bytes into a Data object.

Multiple data types with the same Data object


Most of the time, you would be okay with sending one type of data, but you might want to take advantage of the fact that you can send like double the size of the data you are sending at the moment to include other data.

Lets imagine that we want to send the price of a cat, along with a flag indicating if it is a kitten or a grown up cat.

// Like before, define a variable for the price
// and create a Data object with it
var price = 100.99

var data = withUnsafePointer(to: &price) {
    Data(bytes: $0, count: MemoryLayout.size(ofValue: price))
}
// now, define the kitten flag
var kitten: UInt8 = 1

// Make a Data object with it
let kittenFlagData = Data([kitten])

// append the two objects
data.append(kittenFlagData)

data now has 9 bytes, 8 bytes for the price (which, from the prespective of the consuming party, starts at offset 0).

It also has 1 byte for the flag (which, from the prespective of the consuming party, starts at offset 8).


That's it. Feel free to provide any kind of feedback.

No comments:

Post a Comment