Hey folks,
I'd like to share BinaryParseKit, a Swift package that uses macros to automatically generate parsing logic for structs and enums from byte arrays.
In my daily work, I often need to parse byte arrays into data structures β dozens of them β and writing each parser by hand quickly became tedious and messy. To simplify this, I created a macro-based solution that makes parsing more declarative and readable.
When Apple announced the swift-binary-parsing package, I decided to make BinaryParseKit to integrate with it out of the box β taking advantage of its memory-safety guarantees and convenience. It's currently supporting iOS 26 and Swift 6.2 and above.
Currently, the macro supports code generation for struct and enum:
@ParseStruct
struct BluetoothPacket {
@parse
let packetIndex: UInt8
@parse
let packetCount: UInt8
@parse
let payload: SignalPacket
}
@ParseStruct
struct SignalPacket {
@parse(byteCount: 2, endianness: .big)
let level: UInt32
@parse(byteCount: 6, endianness: .little)
let id: UInt64
@skip(byteCount: 1, because: "padding byte")
@parse(endianness: .big)
let messageSize: UInt8
@parse(byteCountOf: \Self.messageSize)
let message: String
}
extension String: SizedParsable {
public init(parsing input: inout BinaryParsing.ParserSpan, byteCount: Int) throws {
try self.init(parsingUTF8: &input, count: byteCount)
}
}
Then it can parse a byte array ([UInt8] or Data) by calling BluetoothPacket(parsing: data):
let packet = try BluetoothPacket(parsing: data)
Example:
let data: [UInt8] = [
// BluetoothPacket header
0x01, // packetIndex = 1
0x02, // packetCount = 2
// SignalPacket (payload)
0xAA, 0xBB, // level = 43707 (2 bytes of UInt32, big-endian)
0x56, 0x34, 0x12, 0xEF, 0xCD, 0xAB, // id = 0xABCDEF123456 (6 bytes, little-endian)
0x00, // padding byte (skipped)
0x0D, // messageSize = 13
0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x20, // "Hello, "
0x53, 0x77, 0x69, 0x66, 0x74, 0x21 // "Swift!"
]
// Parse it
let packet = try BluetoothPacket(parsing: data)
// Access the fields
print("Packet Index: \(packet.packetIndex)") // 1
print("Packet Count: \(packet.packetCount)") // 2
print("Signal Level: \(packet.payload.level)") // 43707
print("Signal ID: 0x\(String(packet.payload.id, radix: 16))") // 0xabcdef123456
print("Message Size: \(packet.payload.messageSize)") // 13
print("Message: \(packet.payload.message)") // "Hello, Swift!"
To parse an enum, the macros @ParseEnum and various match macros can be used.
@ParseEnum
enum CommandResult: Equatable {
@matchAndTake(byte: 0xAA)
case succeed
@matchAndTake(byte: 0xBB)
@parse(endianness: .big)
@parse(endianness: .big)
case statusUpdate(lat: Float32, long: Float32)
@matchAndTake(byte: 0xCC)
@parse
case failure(reason: UInt8)
@matchDefault
case unknown
}
Parsing works the same way:
let statusUpdateData: [UInt8] = [
0xBB, // statusUpdate command
0x42, 0x22, 0xD6, 0xE5, // lat = 40.709858 (Float32, big-endian)
0xC2, 0x94, 0x03, 0xD7 // long = -74.0075 (Float32, big-endian)
]
let result = try CommandResult(parsing: statusUpdateData)
print("Command Result: \(result)") // statusUpdate(lat: 40.709858, long: -74.0075)
This project is still in its early stages. Iβd really appreciate any feedback or suggestions β Iβm still exploring how useful this approach might be out side of my own use case. I plan to add support for versions prior to iOS 26 by using withUnsafeBytes along with explicit bounds checking in the near future. Thereβs also room for performance improvements, such as optimizing bounds checks and making enum matching more efficient through jump tables.
You can find the repository here, and more examples and documentation here.
Thanks for reading β Iβd love to hear your thoughts, use cases, or ideas for improvement!