1
votes

I’m looking to save a swift struct that includes a dictionary to data using NSKeyedArchiver. The reason I’m using NSKeyedArchiver is because the dictionary has a non-codable variable associated with it. I'm following this guide from Paul Hudson.

The problem I’m running into is that I keep getting the error "The data couldn’t be written because it isn’t in the correct format." at " let encoded = try encoder.encode(test) " This seems to work with non-codable types, but not when they are in a dictovnay. Does anyone know of a way to get this working? Here is the code:

import SwiftUI
import Combine
import HealthKit

struct Testing: View {

    func saveData(){
        let test = TestHealthSample(
            myHKUnit : [Unit.imperial : HKUnit.kilocalorie(), Unit.metric : HKUnit.kilocalorie()],
            isFavorite: true
        )

        let encoder = JSONEncoder()

        do {
            let encoded = try encoder.encode(test)
            let str = String(decoding: encoded, as: UTF8.self)
            print(str)
        } catch {
            print(error.localizedDescription)
        }
    }

    var body: some View {
        Text("Test")
            .onAppear{
                self.saveData()
        }
    }
}


struct TestHealthSample{
    var myHKUnit     : [Unit: HKUnit]
    var isFavorite   : Bool
}


extension TestHealthSample: Codable {

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: TestCodingKeys.self)
        isFavorite = try container.decode(Bool.self, forKey: .isFavorite)


        let hkUnitData = try container.decode(Data.self, forKey: .myHKUnit)
        myHKUnit = try (NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(hkUnitData) as? [Unit:HKUnit]) ?? [Unit.metric : HKUnit.count()]
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: TestCodingKeys.self)
        try container.encode(isFavorite, forKey: .isFavorite)


        let hkUnitData = try NSKeyedArchiver.archivedData(withRootObject: myHKUnit, requiringSecureCoding: false)
        try container.encode(hkUnitData, forKey: .myHKUnit)
    }
}


enum TestCodingKeys: String, CodingKey {
    case myHKUnit
    case isFavorite
}

enum Unit : String, Codable{
    case metric
    case imperial
}
1
Change print(error.localizedDescription) to print(error) to get more detail.rmaddy
Unfortunately that didn't do much. It spits out UserInfo={NSDebugDescription=Caught exception during archival: -[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x283e45ef0 (..... and then a bunch of text after that.Richard Witherspoon
You are mixing up Codable and NSCoding. The latter doesn't work at all with structs. Drop NSKeyedArchiver and use only Codablevadian
@vadian I would but "var myHKUnit : [Unit: HKUnit]" contains HKUnit which is a non-codable typeRichard Witherspoon
Make it conform to Codable if possible or write a wrapper.vadian

1 Answers

2
votes

The problem is the Unit enum which cannot be exposed to Objective-C.

You have to add a logic to map the enum to its raw values and vice versa, something like this

extension TestHealthSample: Codable {

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: TestCodingKeys.self)
        isFavorite = try container.decode(Bool.self, forKey: .isFavorite)
        let hkUnitData = try container.decode(Data.self, forKey: .myHKUnit)
        let myHKUnitObjC = try (NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(hkUnitData) as? [String: HKUnit]) ?? [Unit.metric.rawValue : HKUnit.count()]
        var myHKUnitData = [Unit:HKUnit]()
        for (key, value) in myHKUnitObjC {
            myHKUnitData[Unit(rawValue: key)!] = value
        }
        myHKUnit = myHKUnitData
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: TestCodingKeys.self)
        try container.encode(isFavorite, forKey: .isFavorite)
        var myHKUnitObjc = [String:HKUnit]()
        for (key, value) in myHKUnit {
            myHKUnitObjc[key.rawValue] = value
        }
        let hkUnitData = try NSKeyedArchiver.archivedData(withRootObject: myHKUnitObjc, requiringSecureCoding: false)
        try container.encode(hkUnitData, forKey: .myHKUnit)
    }
}