How to: Work with raw binary data inside your Swift applications

Martin Albrecht
5 min readJul 15, 2023
Photo by Pietro Jeng on Unsplash

When it comes to the task of reading in raw binary data I usually write a quick program in C (yes I am old), but when I recently came to such a task I thought: Apple advertises Swift as a system language so it should be able to do the job equally, if not even better.

It turned out I was right and Swift was not only better, it was pretty straightforward writing a program for my needs. But let’s start from the beginning.

The data structure

Let’s say we have a bunch of data in the form of a binary file (for example sensor data from some arbitrary micro controller) and we want to work with that data inside a program written in Swift.

Opening the file in a hex editor would result in something like this:

Screenshot of the data inside a hex editor
The data inside a hex editor

This non readable hex data is not really helpful as it is. To work with this data we first need to clarify the structure (e.g. data types and their sizes) and convert it to the according types in Swift.

Side note: Hex Fiend (https://hexfiend.com/) has the nice feature to display the actual value of the currently selected bytes in the editor’s status bar.

The data frame

For this example we assume the following format for every frame of data:

Tabular view of the structure of our data
Structure of a data frame

According to this we have a total size of 60 bytes per frame with each data type being 4 bytes wide (32 bit). This information is required when you want to work with more than one frame, but for the sake of simplicity of this tutorial we will only focus on how to read single fields of a frame of data.

Note: The binary data is a constant stream of data, so you need to split it yourself, if you want to work with multiple data frames. There are no delimiters!

Reading the data

Opening and reading a file in Swift is fortunately quite easy. Assuming our file is in the same folder as our script and named “DATA.DAT”, we can start reading the file with the following code:

import Foundation

do {
let data = try Data(contentsOf: URL(fileURLWithPath: “DATA.DAT"))
} catch {
fatalError(error.localizedDescription)
}

Note: If you’re running your code inside a XCode project, your URL needs to be based on the Bundle. See https://developer.apple.com/documentation/foundation/bundle for more information. This tutorial is intended for using the script on the command line.

If no error occurs this code will read in the whole file and set the variable data to a Data object (e.g. the raw bytes of the file).

As we saw before, the first field of our data is a 4 bytes wide long, so the first 4 bytes of our data object combined together should result in the value of that long

We know the format of the data, so we could now tend to think that accessing the frames could be easily done by just initializing an Int32 (which is 4 bytes wide) with the initializer accessing a subscript of the data like:

let value = Int32(data[0...3])

Unfortunately it’s not that easy. Running the code would result in errors:

error: no exact matches in call to initializer 
let value = Int32(data[0...3])
^
Swift.FixedWidthInteger:3:23: note: candidate requires that 'Data' conform to 'BinaryFloatingPoint' (requirement specified as 'T' : 'BinaryFloatingPoint')
@inlinable public init<T>(_ source: T) where T : BinaryFloatingPoint
^
Swift.SignedInteger:2:23: note: candidate requires that 'Data' conform to 'BinaryInteger' (requirement specified as 'T' : 'BinaryInteger')
@inlinable public init<T>(_ source: T) where T : BinaryInteger
^
Swift.FixedWidthInteger:2:23: note: candidate requires that 'Data' conform to 'StringProtocol' (requirement specified as 'S' : 'StringProtocol')
@inlinable public init?<S>(_ text: S, radix: Int = 10) where S : StringProtocol

As we can see, the Int32 object in Swift has no initializer for a an array (or slice of array) of bytes, which is what we got from reading in the file. So how can we handle that?

Converting the data

To work with the data we need to convert it into matching data types for the respective field. In our example all fields are four bytes wide and we have two types of data: Integer and floating point values.

Swift provides us with two primitive types which give us the correct size: Int32 and Float32.

To read in the raw bytes of our data and convert them, we need to make use of the withUnsafeBytes(_:) method of the Data object in combination of the load method of the UnsafeRawBufferPointer object which is passed to the closure of the method. You can find more information in the documentation.

A simple extension of the Data object can help us with our problem:

extension Data {
var int32: Int32 { withUnsafeBytes({ $0.load(as: Int32.self) }) }
}

With this extension we can now transform the first four bytes of our data to the according data type:

do {
let data = try Data(contentsOf: URL(fileURLWithPath: "DATA.DAT"))
let value = data[0...3].int32

dump(value)
} catch {
fatalError(error.localizedDescription)
}

This code will print out the correct value of the long, for example:

➜ swift convert-data.swift 
- 2935

For floating point values we can apply the same principle and extend our extension:

extension Data {
var int32: Int32 { withUnsafeBytes({ $0.load(as: Int32.self) }) }
var float32: Float32 { withUnsafeBytes({ $0.load(as: Float32.self) }) }
}

With that we can now convert all data into according types and theoretically read in the whole file — but that part would be up to you. 😉

The complete code

A complete example for reading out an integer and a float value from our data:

import Foundation

extension Data {
var int32: Int32 { withUnsafeBytes({ $0.load(as: Int32.self) }) }
var float32: Float32 { withUnsafeBytes({ $0.load(as: Float32.self) }) }
}

do {
let data = try Data(contentsOf: URL(fileURLWithPath: "DATA.DAT"))
let value = data[0...3].int32
let value2 = data[16...19].float32

dump(value)
dump(value2)
} catch {
fatalError(error.localizedDescription)
}

If you wonder why the second offset is so high (16–20) and skips some fields, this is only because based on our data format from before we have 16 bytes of integer data at the beginning of each frame. At byte 16 the first floating point value can be found and our second example is just for demonstrating working with float data.

--

--