What have I done?
I read over Apple's documentation on the topic.
Details
The Swift standard library defines a standardized approach to data encoding and decoding.
I realized that all the examples I have come across decode data from an external data representation that has keys like JSON
, PLIST
, and XML
(custom).
I came across a use case where the external data representation doesn't have keys.
For example, when reading data from bluetooth devices. You receive data formatted in an Array
of UInt8
.
When exploring Apple's documentation and custom Decoder
implementations I realized that all external data representations have keys.
I am wondering if Apple's solution leaves enough wiggle room to work with an external data representation that doesn't use keys i.e an Array
of UInt8
.
I think you might have misunderstood what the Codable API is for. It is not for writing the logic for parsing and writing the binary data - you still have to write that yourself.
Codable
allows you to encode and decode in a more abstract way, through keyed, unkeyed, and single value containers. You can say things like:
You write how you want your models to be encoded/decoded in terms of these operations, then the Decoder
/Encoder
will do the corresponding thing to the underlying binary data.
For example, if you want to decode a JSON array with 5 integers, into a struct with 5 Int
properties, you write:
struct MyModel: Decodable {
let a, b, c, d, e: Int
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
a = try container.decode(Int.self)
b = try container.decode(Int.self)
c = try container.decode(Int.self)
d = try container.decode(Int.self)
e = try container.decode(Int.self)
}
}
For JSON, unkeyedContainer()
means "get ready to decode a JSON array", and "decode(Int.self)" means "read the next array element as an Int
". Note that you don't have to write how exactly to parse the JSON.
Decoder
/Encoder
implementations doesn't have to support all of those operations. If the implementation doesn't support some method for any reason, they can throw an error when that method is called.
For a series of bytes, you can implement your own Decoder
that supports unkeyedContainer()
only, and decode(T.self)
would read x number of bytes depending on T
, and convert that into the correct type, a bit like a BinaryReader.
// just rough sketch of an implementation, not complete
struct _BinaryDecoder: Decoder, UnkeyedDecodingContainer {
var isAtEnd: Bool {
data.count >= currentIndex
}
var currentIndex: Int = 0
func hasNBytes(_ n: Int) -> Bool {
data.count - currentIndex >= n
}
mutating func decode(_ type: Float.Type) throws -> Float {
guard hasNBytes(4) else { throw someError }
let f = data.withUnsafeBytes { buffer in
buffer.loadUnaligned(fromByteOffset: currentIndex, as: Float.self)
}
currentIndex += 4
return f
}
mutating func decode(_ type: Int32.Type) throws -> Int32 {
guard hasNBytes(4) else { throw someError }
let i = data.withUnsafeBytes { buffer in
buffer.loadUnaligned(fromByteOffset: currentIndex, as: Int32.self)
}
currentIndex += 4
return i
}
let data: Data
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
self
}
}
struct BinaryDecoder {
func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
try T.init(from: _BinaryDecoder(data: data))
}
}
You can then implement init(from:)
in your model like this:
var container = try decoder.unkeyedContainer()
let flags = try container.decode(UInt16.self)
switch flags {
// use container.decode to initialise different properties based on flags
// e.g. if the next bytes represent two 32-bit signed ints, and then a float
case 0:
property1 = try container.decode(Int32.self)
property2 = try container.decode(Int32.self)
property3 = try container.decode(Float.self)
}
Though, if you are only ever going to decode this one type of binary data, it's probably easier to just write your decoding logic without using Codable
.