在 UserDefaults 中保存不同对象类型的数组

Posted

技术标签:

【中文标题】在 UserDefaults 中保存不同对象类型的数组【英文标题】:Save array of different object types in UserDefaults 【发布时间】:2021-09-23 07:57:06 【问题描述】:

我有两个类,Radio 和 Podcast,Media 类的孩子。 我正在尝试将一系列媒体(带有收音机和播客)保存到 UserDefaults,但是当我取回它时,我只有媒体(我正在丢失收音机或播客的信息)。 我无法将这些项目投射到 Radio 或 Podcast。

    private func saveRecentMediaInData(_ medias:[Media]) 
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(medias) 
            UserDefaults.standard.setValue(encoded, forKey: recentMediasKey)
        
    
    
    private func getRecentMediasFromData() -> [Media] 
        let defaults = UserDefaults.standard
        if let data = defaults.value(forKey: recentMediasKey) as? Data 
            let decoder = JSONDecoder()
            if let decoded = try? decoder.decode(Array.self, from: data) as [Media] 
                return decoded
            
        
        return []
    

谢谢

【问题讨论】:

【参考方案1】:

该问题与UserDefaults 无关。它有一个混合对象数组,要使用Codable 解码。

在这种情况下,解决方案是使用带有关联值的enum

enum Mixed: Codable 

    case radio(Radio)
    case podcast(Podcast)

    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        if let asRadio = try? container.decode(Radio.self) 
            self = .radio(asRadio)
         else if let asPodcast = try? container.decode(Podcast.self) 
            self = .podcast(asPodcast)
         else 
            fatalError("Oops")
        
    

这是一个完整的示例代码:

struct SubClassesCodable 
    class Media: Codable, CustomStringConvertible 
        var title: String

        var description: String 
            return "Media: \(title)"
        
    
    class Radio: Media 
        var channel: Int
        enum CodingKeys: String, CodingKey 
            case channel
        

        required init(from decoder: Decoder) throws 
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.channel = try container.decode(Int.self, forKey: .channel)
            try super.init(from: decoder)
        

        override func encode(to encoder: Encoder) throws 
            try super.encode(to: encoder)
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(channel, forKey: .channel)
        

        override var description: String 
            return "Radio: \(title) - \(channel)"
        
    
    class Podcast: Media 
        var author: String
        enum CodingKeys: String, CodingKey 
            case author
        

        required init(from decoder: Decoder) throws 
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.author = try container.decode(String.self, forKey: .author)
            try super.init(from: decoder)
        

        override func encode(to encoder: Encoder) throws 
            try super.encode(to: encoder)
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(author, forKey: .author)
        

        override var description: String 
            return "Podcast: \(title) - \(author)"
        
    

    enum Mixed: Codable 

        case radio(Radio)
        case podcast(Podcast)

        init(from decoder: Decoder) throws 
            let container = try decoder.singleValueContainer()
            if let asRadio = try? container.decode(Radio.self) 
                self = .radio(asRadio)
             else if let asPodcast = try? container.decode(Podcast.self) 
                self = .podcast(asPodcast)
             else 
                fatalError("Oops") //Or rather throws a custom error
            
        
    

    static func test() 
        let mediaJSONStr = #""title": "media""#
        let radioJSONStr = #""title": "radio", "channel": 3"#
        let podcastJSONStr = #""title": "podcast", "author": "myself""#

        do 
            let decoder = JSONDecoder()
            //Create values from JSON
            let media = try decoder.decode(Media.self, from: Data(mediaJSONStr.utf8))
            print(media)
            let radio = try decoder.decode(Radio.self, from: Data(radioJSONStr.utf8))
            print(radio)
            let podcast = try decoder.decode(Podcast.self, from: Data(podcastJSONStr.utf8))
            print(podcast)
            let array: [Media] = [radio, podcast]
            print(array)

            // Encode to Data, that's what's saved into UserDefaults
            let encoder = JSONEncoder()
            let encodedArray = try encoder.encode(array)
            print("Encoded: \(String(data: encodedArray, encoding: .utf8)!)") //It's more readable as JSON String than Data

            //This will fail, it's the current author code
            let decoded = try decoder.decode([Media].self, from: encodedArray)
            print(decoded)
            decoded.forEach 
                if let asRadio = $0 as? Radio 
                    print(asRadio)
                else if let asPodcast = $0 as? Podcast 
                    print(asPodcast)
                 else 
                    print("Nop: \($0)")
                
            

            //This is a working solution
            let mixedDecoded = try decoder.decode([Mixed].self, from: encodedArray)
            let decodedArray: [Media] = mixedDecoded.map 
                switch $0 
                case .radio(let radio):
                    return radio
                case .podcast(let podcast):
                    return podcast
                
            
            print(decodedArray)

         catch 
            print("Error: \(error)")
        
    


SubClassesCodable.test()

【讨论】:

以上是关于在 UserDefaults 中保存不同对象类型的数组的主要内容,如果未能解决你的问题,请参考以下文章

将对象保存到确认协议的 UserDefaults(使用面向协议的编程范例)

尝试使用 NSKeyedArchiver 在 UserDefaults 中保存自定义对象

使用 UserDefaults 为不同用户存储数据

UserDefaults 自定义对象的自定义对象

Swift: 用UserDefaults保存复杂对象

使用 UserDefaults 保存数据的正确事件是啥?