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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 18:15:05