You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用NSKeyedArchiver保存含非Codable字典的Swift结构体报错求助

问题:用Codable和NSKeyedArchiver归档包含HKUnit的字典时出错

我尝试使用NSKeyedArchiver将包含字典的Swift结构体保存为数据,选择这个工具是因为字典里关联了非Codable类型HKUnit。参照教程实现后,在执行let encoded = try encoder.encode(test)时持续报错:The data couldn’t be written because it isn’t in the correct format.。单独处理非Codable类型时可正常运行,但放入字典后失效,请问该如何解决?

相关代码如下:

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
}

问题原因

你遇到的核心问题是:NSKeyedArchiver无法直接归档[Unit: HKUnit]类型的字典。虽然HKUnit实现了NSCoding协议,但你的自定义枚举Unit仅遵循Codable,并没有实现Objective-C的NSCoding协议,也不是Objective-C原生兼容的类型(比如NSString、NSNumber这类)。NSKeyedArchiver处理字典时,要求字典的Key必须是Objective-C可识别的类型,否则会归档失败,进而导致后续的JSONEncoder编码报错。


解决方案

我们可以通过将字典的Key转换为Objective-C兼容的String类型来解决这个问题:

  1. 编码时,把[Unit: HKUnit]转换成[String: HKUnit](利用Unit枚举的rawValue属性),再归档这个字符串Key的字典;
  2. 解码时,先将归档的数据解档为[String: HKUnit],再转换回[Unit: HKUnit]。

修复后的完整代码如下:

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)
        // 1. 先解档为[String: HKUnit]类型
        guard let stringKeyedDict = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(hkUnitData) as? [String: HKUnit] else {
            myHKUnit = [Unit.metric : HKUnit.count()]
            return
        }
        // 2. 将String类型的Key转换回Unit枚举
        var convertedDict = [Unit: HKUnit]()
        for (rawKey, unit) in stringKeyedDict {
            if let unitEnum = Unit(rawValue: rawKey) {
                convertedDict[unitEnum] = unit
            }
        }
        myHKUnit = convertedDict.isEmpty ? [Unit.metric : HKUnit.count()] : convertedDict
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: TestCodingKeys.self)
        try container.encode(isFavorite, forKey: .isFavorite)
        
        // 1. 将[Unit: HKUnit]转换为[String: HKUnit]类型
        let stringKeyedDict = Dictionary(uniqueKeysWithValues: myHKUnit.map { ($0.key.rawValue, $0.value) })
        // 2. 归档转换后的字典
        let hkUnitData = try NSKeyedArchiver.archivedData(withRootObject: stringKeyedDict, requiringSecureCoding: false)
        try container.encode(hkUnitData, forKey: .myHKUnit)
    }
}

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

enum Unit : String, Codable{
    case metric
    case imperial
}

额外说明

  • 如果你需要更严谨的错误处理,可以在转换失败时抛出自定义错误,而不是直接使用默认值;
  • 注意requiringSecureCoding: false的使用:如果你的项目要求安全编码,由于HKUnit本身支持安全编码,你可以将该参数设为true。

内容的提问来源于stack exchange,提问作者Richard Witherspoon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:07:59