Home > other >  Save array of different object types in UserDefaults
Save array of different object types in UserDefaults

Time:09-23

I have two classes, Radio and Podcast, children of the Media class. I'm trying to save an array of Media (with radios and podcasts) to UserDefaults but when I get it back, I only have medias (I'm losing information of Radio or Podcast). I cannot cast the items to Radio or 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 []
    }

Thanks

CodePudding user response:

The issue is not related to UserDefaults. It's having an array of mixed object to decode with Codable.

In this case, a solution is to use a enum with associated value:

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")
        }
    }
}

Here is a full sample code:

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()
  • Related