Swift中向UserDefaults存储含不同子类的Media数组后丢失子类信息问题
问题根因
Swift 原生 JSONEncoder/JSONDecoder 不默认支持类继承的多态编解码:声明为[Media]类型的数组在序列化时,编码器只会读取基类Media的属性进行编码,不会识别子类的特有属性;解码时也只会生成基类Media的实例,自然无法向下转型为Radio或Podcast子类,也不会保留子类属性。
方案1:手动实现Codable多态适配(纯Swift类推荐)
步骤1:新增类型标识字段与基类编解码逻辑
首先给Media基类新增MediaType枚举标识实例类型,同时手动实现Codable协议:
enum MediaType: String, Codable { case radio case podcast } class Media: Codable { // 基类原有属性 let id: String let name: String // 类型标识字段 let type: MediaType enum CodingKeys: String, CodingKey { case id, name, type } init(id: String, name: String, type: MediaType) { self.id = id self.name = name self.type = type } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) name = try container.decode(String.self, forKey: .name) type = try container.decode(MediaType.self, forKey: .type) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(name, forKey: .name) try container.encode(type, forKey: .type) } }
步骤2:子类手动实现编解码逻辑
每个子类需要单独实现自己的Codable逻辑,调用父类的编解码方法完成基类属性处理:
class Radio: Media { // 子类特有属性 let frequency: Double enum CodingKeys: String, CodingKey { case frequency } init(id: String, name: String, frequency: Double) { self.frequency = frequency super.init(id: id, name: name, type: .radio) } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) frequency = try container.decode(Double.self, forKey: .frequency) try super.init(from: container.superDecoder()) } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(frequency, forKey: .frequency) try super.encode(to: container.superEncoder()) } } // Podcast子类同理实现 class Podcast: Media { // 子类特有属性 let episodeCount: Int enum CodingKeys: String, CodingKey { case episodeCount } init(id: String, name: String, episodeCount: Int) { self.episodeCount = episodeCount super.init(id: id, name: name, type: .podcast) } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) episodeCount = try container.decode(Int.self, forKey: .episodeCount) try super.init(from: container.superDecoder()) } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(episodeCount, forKey: .episodeCount) try super.encode(to: container.superEncoder()) } }
步骤3:改造解码逻辑,根据类型标识生成对应子类实例
原有解码方法直接解码为[Media]只会生成基类实例,需要修改为逐个读取类型标识后解码为对应子类:
private func getRecentMediasFromData() -> [Media] { guard let data = UserDefaults.standard.data(forKey: recentMediasKey) else { return [] } do { var unkeyedContainer = try JSONDecoder().decode(UnkeyedDecodingContainer.self, from: data) var result = [Media]() while !unkeyedContainer.isAtEnd { // 先读取当前元素的类型标识 let itemContainer = try unkeyedContainer.nestedContainer(keyedBy: Media.CodingKeys.self) let type = try itemContainer.decode(MediaType.self, forKey: .type) // 按类型解码为对应子类 switch type { case .radio: result.append(try unkeyedContainer.decode(Radio.self)) case .podcast: result.append(try unkeyedContainer.decode(Podcast.self)) } } return result } catch { print("解码失败: \(error.localizedDescription)") return [] } }
存储逻辑不需要修改,直接使用原有saveRecentMediaInData方法即可。
方案2:使用NSCoding序列化(类继承自NSObject时推荐)
如果你的Media类继承自NSObject,可以使用NSKeyedArchiver完成序列化,原生支持多态,不需要额外处理类型标识:
class Media: NSObject, NSCoding { let id: String let name: String init(id: String, name: String) { self.id = id self.name = name } func encode(with coder: NSCoder) { coder.encode(id, forKey: "id") coder.encode(name, forKey: "name") } required init?(coder: NSCoder) { guard let id = coder.decodeObject(forKey: "id") as? String, let name = coder.decodeObject(forKey: "name") as? String else { return nil } self.id = id self.name = name } } // Radio子类实现示例 class Radio: Media { let frequency: Double init(id: String, name: String, frequency: Double) { self.frequency = frequency super.init(id: id, name: name) } required init?(coder: NSCoder) { frequency = coder.decodeDouble(forKey: "frequency") super.init(coder: coder) } override func encode(with coder: NSCoder) { coder.encode(frequency, forKey: "frequency") super.encode(with: coder) } }
对应的存取方法修改为:
private func saveRecentMediaInData(_ medias:[Media]) { do { let data = try NSKeyedArchiver.archivedData(withRootObject: medias, requiringSecureCoding: false) UserDefaults.standard.set(data, forKey: recentMediasKey) } catch { print("存储失败: \(error.localizedDescription)") } } private func getRecentMediasFromData() -> [Media] { guard let data = UserDefaults.standard.data(forKey: recentMediasKey) else { return [] } do { return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [Media] ?? [] } catch { print("读取失败: \(error.localizedDescription)") return [] } }
内容的提问来源于stack exchange,提问作者Edwin ZAP
相关产品推荐
相关产品推荐

