使用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类型来解决这个问题:
- 编码时,把
[Unit: HKUnit]转换成[String: HKUnit](利用Unit枚举的rawValue属性),再归档这个字符串Key的字典; - 解码时,先将归档的数据解档为
[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
相关产品推荐
相关产品推荐

