Swift中使用Codable保存泛型结构到UIDocument编码报错
问题:自定义泛型结构体Tree编码保存到UIDocument时抛出unrecognized selector错误
错误信息
运行时抛出如下崩溃日志:
EigenFlow[81334:7048816] -[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x60000195aee0
现有实现代码
UIDocument子类实现
class Document: UIDocument { var saveUniqueTree : Tree<Unique<Int>> = uniqueTree var objectsArray = [Any]() override func contents(forType typeName: String) throws -> Any { self.objectsArray.removeAll() self.objectsArray.append(saveUniqueTree) print("ObjectsArray After: \(objectsArray)") let graphData = try NSKeyedArchiver.archivedData(withRootObject: objectsArray, requiringSecureCoding: false) return graphData } }
自定义类型Codable实现
struct Tree<A: Codable>: Codable { var value: A var children: [Tree<A>] = [] enum CodingKeys: String, CodingKey { case value, children } init(_ value: A, children: [Tree<A>] = []) { self.value = value self.children = children } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) value = try values.decode(A.self, forKey: .value) children = try values.decode([Tree<A>].self, forKey: .children) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(value, forKey: .value) try container.encode(children, forKey: .children) } } class Unique<A: Codable>: Identifiable, Codable { var value: A enum CodingKeys: String, CodingKey { case value } init(_ value: A) { self.value = value } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) do { try container.encode(value, forKey: .value) } catch { fatalError("Should never happen") } } } struct Diagram<A: Codable & Identifiable, V: View>: View { let tree: Tree<A> let node: (A) -> V typealias Key = CollectDict<A.ID, Anchor<CGPoint>> var body: some View { VStack(alignment: .center) { node(tree.value) .anchorPreference(key: Key.self, value: .center, transform: { [self.tree.value.id: $0] }) HStack(alignment: .bottom, spacing: 10) { ForEach(tree.children, id: \.value.id, content: { child in Diagram(tree: child, node: self.node) }) } }.backgroundPreferenceValue(Key.self, { (centers: [A.ID: Anchor<CGPoint>]) in GeometryReader { proxy in ForEach(self.tree.children, id: \.value.id, content: { child in let childValue = tree.value Line( from: proxy[centers[self.tree.value.id]!], to: proxy[centers[child.value.id]!]) .stroke(Color.init(grayBarColor), style: StrokeStyle(lineWidth: 7)) .onAppear { } }) } }) } } var uniqueTree: Tree<Unique<Int>> = binaryTree.map(Unique.init)
全局变量uniqueTree支持全局任意位置访问,需求是正常将uniqueTree编码保存到UIDocument中,不触发上述崩溃,确认当前实现的遗漏问题。
问题根因
- 核心错误是混淆了
Codable协议和NSCoding协议:NSKeyedArchiver是Objective-C时代遗留的归档工具,只支持遵守NSCoding(或NSSecureCoding)协议的NSObject子类类型,不支持Swift原生的Codable类型。- 定义的
Tree是Swift结构体、Unique虽然是类但没有继承NSObject也没有实现NSCoding相关方法,直接塞给NSKeyedArchiver时,系统会把Swift值类型桥接成不响应encodeWithCoder:的__SwiftValue类型,直接触发找不到选择器的崩溃。
- 额外问题:
Unique类的Codable实现缺失init(from:)初始化方法,就算用正确的Codable归档方式也会报错。代码中声明的[Any]类型数组也不支持Codable归档,无实际存在必要。
修复方案
方案1:用Swift原生编码器替代NSKeyedArchiver(推荐)
这是Swift类型归档的标准方案,不需要改动现有Codable核心实现,只需要补全Unique类缺失的解码器方法,再修改UIDocument内的归档逻辑即可。
- 补全
Unique类的init(from:)实现:
required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) value = try container.decode(A.self, forKey: .value) }
- 修改
contents(forType:)方法内的归档逻辑,用JSONEncoder替换NSKeyedArchiver,移除无用的objectsArray:
override func contents(forType typeName: String) throws -> Any { let encoder = JSONEncoder() // 如果需要更小的二进制体积,可替换为PropertyListEncoder let graphData = try encoder.encode(saveUniqueTree) return graphData }
- 实现对应的读取逻辑
load(fromContents:),用JSONDecoder解码:
override func load(fromContents contents: Any, ofType typeName: String?) throws { guard let data = contents as? Data else { throw NSError(domain: "DocumentError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的文档数据"]) } let decoder = JSONDecoder() saveUniqueTree = try decoder.decode(Tree<Unique<Int>>.self, from: data) }
方案2:如果必须使用NSKeyedArchiver
需要让所有要归档的类型继承NSObject并遵守NSSecureCoding协议,手动实现encode(with coder:)和init?(coder:)方法,这种方式对Swift泛型结构体适配成本极高,容易出现兼容性问题,不推荐使用。
内容的提问来源于stack exchange,提问作者Paul Curtis
相关产品推荐
相关产品推荐

