iosarraysswiftnskeyedarchiver

How can I encode an array of simd_float4x4 elements in Swift (convert simd_float4x4 to Data)?


I am building an app that captures facetracking data from the iPhone TrueDepth camera.

I need to write this data to files so I can use it as the basis for another app.

Within the app, the data is saved into four separate arrays, one containing ARFaceGeometry objects, and the other three with transform coordinates as simd_float4x4 matrices.

I am converting the arrays into Data objects using archivedData(withRootObject: requiringSecureCoding:) then calling write(to:) on them to create the files.

The file containing the ARFaceGeometry data is written and read back in correctly. But the three simd_float4x4 arrays aren't being written, even though the code for doing so is identical. Along with my print logs, the error being given is 'unrecognized selector sent to instance'.

Properties:

var faceGeometryCapture = [ARFaceGeometry]()
var faceTransformCapture = [simd_float4x4]()
var leftEyeTransformCapture = [simd_float4x4]()
var rightEyeTransformCapture = [simd_float4x4]()

var faceGeometryCaptureFilePath: URL!
var faceTransformCaptureFilePath: URL!
var leftEyeTransformCaptureFilePath: URL!
var rightEyeTransformCaptureFilePath: URL!

Code for establishing file URLs:

let fileManager = FileManager.default
let dirPaths = fileManager.urls(for: .documentDirectory,
                        in: .userDomainMask)
        
faceGeometryCaptureFilePath = dirPaths[0].appendingPathComponent("face-geometries.txt")
faceTransformCaptureFilePath = dirPaths[0].appendingPathComponent("face-transforms.txt")
leftEyeTransformCaptureFilePath = dirPaths[0].appendingPathComponent("left-eye-transforms.txt")
rightEyeTransformCaptureFilePath = dirPaths[0].appendingPathComponent("right-eye-transforms.txt")

Code for writing the data to files:

do {
    let data = try NSKeyedArchiver.archivedData(withRootObject: faceGeometryCapture, requiringSecureCoding: false)
    try data.write(to: faceGeometryCaptureFilePath)
    } catch { print("Error writing face geometries to file") }
do {
    let data = try NSKeyedArchiver.archivedData(withRootObject: faceTransformCapture, requiringSecureCoding: false)
    try data.write(to: faceTransformCaptureFilePath)
    } catch { print("Error writing face transforms to file") }
do {
    let data = try NSKeyedArchiver.archivedData(withRootObject: leftEyeTransformCapture, requiringSecureCoding: false)
    try data.write(to: leftEyeTransformCaptureFilePath)
    } catch { print("Error writing left eye transforms to file") }
do {
    let data = try NSKeyedArchiver.archivedData(withRootObject: rightEyeTransformCapture, requiringSecureCoding: false)
    try data.write(to: rightEyeTransformCaptureFilePath)
    } catch { print("Error writing right eye transforms to file") }

I'm guessing it's the simd_float4x4 struct that is causing the issue, as this is the only difference between working and not working. Can anyone confirm and suggest a solution?

Thanks in advance.


Solution

  • As already mentioned in comments structures can't conform to NSCoding but you can make simd_float4x4 conform to Codable and persist its data:

    extension simd_float4x4: Codable {
        public init(from decoder: Decoder) throws {
            var container = try decoder.unkeyedContainer()
            try self.init(container.decode([SIMD4<Float>].self))
        }
        public func encode(to encoder: Encoder) throws {
            var container = encoder.unkeyedContainer()
            try container.encode([columns.0,columns.1, columns.2, columns.3])
        }
    }
    

    Playground testing:

    do {
        let vector = simd_float4x4(2.7)  // simd_float4x4([[2.7, 0.0, 0.0, 0.0], [0.0, 2.7, 0.0, 0.0], [0.0, 0.0, 2.7, 0.0], [0.0, 0.0, 0.0, 2.7]])
        let data = try JSONEncoder().encode(vector)  // 111 bytes
        let json = String(data: data, encoding: .utf8)
        print(json ?? "")  // [[[2.7000000476837158,0,0,0],[0,2.7000000476837158,0,0],[0,0,2.7000000476837158,0],[0,0,0,2.7000000476837158]]]\n"
        let decoded = try JSONDecoder().decode(simd_float4x4.self, from: data)
        print(decoded)     // "simd_float4x4([[2.7, 0.0, 0.0, 0.0], [0.0, 2.7, 0.0, 0.0], [0.0, 0.0, 2.7, 0.0], [0.0, 0.0, 0.0, 2.7]])\n"
        decoded == vector  // true
    } catch {
        print(error)
    }
    

    edit/update:

    Another option is to save its raw bytes. It will use only 64 bytes:

    extension simd_float4x4: ContiguousBytes {
        public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
            try Swift.withUnsafeBytes(of: self) { try body($0) }
        }
    }
    

    extension ContiguousBytes {
        init<T: ContiguousBytes>(_ bytes: T) {
            self = bytes.withUnsafeBytes { $0.load(as: Self.self) }
        }
        var bytes: [UInt8] { withUnsafeBytes { .init($0) } }
        var data: Data { withUnsafeBytes { .init($0) } }
        func object<T>() -> T { withUnsafeBytes { $0.load(as: T.self) } }
        func objects<T>() -> [T] { withUnsafeBytes { .init($0.bindMemory(to: T.self)) } }
        var simdFloat4x4: simd_float4x4 { object() }
        var simdFloat4x4Collection: [simd_float4x4] { objects() }
    }
    

    extension Array where Element: ContiguousBytes {
        var bytes: [UInt8] { withUnsafeBytes { .init($0) } }
        var data: Data { withUnsafeBytes { .init($0) } }
    }
    

    let vector1 = simd_float4x4(.init(2, 1, 1, 1), .init(1, 2, 1, 1), .init(1, 1, 2, 1), .init(1, 1, 1, 2))
    let vector2 = simd_float4x4(.init(3, 1, 1, 1), .init(1, 3, 1, 1), .init(1, 1, 3, 1), .init(1, 1, 1, 3))
    let data = [vector1,vector2].data          // 128 bytes
    let loaded = data.simdFloat4x4Collection
    print(loaded)    // "[simd_float4x4([[2.0, 1.0, 1.0, 1.0], [1.0, 2.0, 1.0, 1.0], [1.0, 1.0, 2.0, 1.0], [1.0, 1.0, 1.0, 2.0]]), simd_float4x4([[3.0, 1.0, 1.0, 1.0], [1.0, 3.0, 1.0, 1.0], [1.0, 1.0, 3.0, 1.0], [1.0, 1.0, 1.0, 3.0]])]\n"
    loaded[0] == vector1  // true
    loaded[1] == vector2  // true