5

Follow up from my previously question:

I managed to make my enum to conform to Codable protocol, implemented init() and encode() and it seems to work.

enum UserState {
    case LoggedIn(LoggedInState)
    case LoggedOut(LoggedOutState)
}

enum LoggedInState: String {
    case playing
    case paused
    case stopped
}

enum LoggedOutState: String {
    case Unregistered
    case Registered
}

extension UserState: Codable {
    enum CodingKeys: String, CodingKey {
        case loggedIn
        case loggedOut
    }

    enum CodingError: Error {
        case decoding(String)

    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        if let loggedIn = try? values.decode(String.self, forKey: .loggedIn) {
            self = .LoggedIn(LoggedInState(rawValue: loggedIn)!)
        }
        else if let loggedOut = try? values.decode(String.self, forKey: .loggedOut) {
            self = .LoggedOut(LoggedOutState(rawValue: loggedOut)!)
        }
        else {
            throw CodingError.decoding("Decoding failed")
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        switch self {
        case let .LoggedIn(value):
            try container.encode(value.rawValue, forKey: .loggedIn)
        case let .LoggedOut(value):
            try container.encode(value.rawValue, forKey: .loggedOut)
        }
    }
}



class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let state = UserState.LoggedIn(.playing)
        UserDefaults.standard.set(state, forKey: "state")
    }
}

My problem is I don't know how to save it to UserDefaults. If I just save it like I do now I get the following error when running the app:

[User Defaults] Attempt to set a non-property-list object Codable.UserState.LoggedIn(Codable.LoggedInState.playing) as an NSUserDefaults/CFPreferences value for key state
2018-01-20 19:06:26.909349+0200 Codable[6291:789687]
djromero
  • 19,551
  • 4
  • 71
  • 68
Adrian
  • 19,440
  • 34
  • 112
  • 219

3 Answers3

4

From UserDefaults reference:

A default object must be a property list—that is, an instance of (or for collections, a combination of instances of) NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData.

So you should encode state manually and store it as data:

if let encoded = try? JSONEncoder().encode(state) {
    UserDefaults.standard.set(encoded, forKey: "state")
}

Then, to read it back:

guard let data = UserDefaults.standard.data(forKey: "state") else { fatalError() }
if let saved = try? JSONDecoder().decode(UserState.self, from: data) {
    ...
}
djromero
  • 19,551
  • 4
  • 71
  • 68
  • Don’t use value(forKey:) UserDefaults has specific method to load data type values in this case data(forKey:) and consequently no need to conditionally cast from `Any?` to `Data`, all you need is to unwrap its value. `guard let data = UserDefaults.standard.data(forKey: "state") else {` – Leo Dabus Jan 20 '18 at 18:49
  • @LeoDabus Thanks. I totally forgot about those specific methods, I updated the answer. – djromero Jan 20 '18 at 18:56
  • @LeoDabus That's up to OP (maybe the `UserState` value is bound to an `UISwitch` state) In any case it is immaterial to my answer and more relevant as comment in the original question, IMHO. – djromero Jan 20 '18 at 20:00
2

Swift 4+. Enum with a normal case, and a case with associated value.

  1. Enum's code:
enum Enumeration: Codable {
    case one
    case two(Int)

    private enum CodingKeys: String, CodingKey {
        case one
        case two
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        if let value = try? container.decode(String.self, forKey: .one), value == "one" {
            self = .one
            return
        }

        if let value = try? container.decode(Int.self, forKey: .two) {
            self = .two(value)
            return
        }

        throw _DecodingError.couldNotDecode
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        switch self {
        case .one:
            try container.encode("one", forKey: .one)
        case .two(let number):
            try container.encode(number, forKey: .two)
        }
    }
}
  1. Write to UserDefaults:
let one1 = Enumeration.two(442)

if let encoded = try? encoder.encode(one1) {
    UserDefaults.standard.set(encoded, forKey: "savedEnumValue")
}
  1. Read from UserDefaults:
if let savedData = UserDefaults.standard.object(forKey: "savedEnumValue") as? Data {
    if let loadedValue = try? JSONDecoder().decode(Enumeration.self, from: savedData) {
        print(loadedValue) // prints: "two(442)"
    }
}
Tung Fam
  • 7,899
  • 4
  • 56
  • 63
0

Enum should have rawValue to have ability to be saved as Codable object. Your enum cases have associated values, so they cannot have rawValue.

Article with good explanation

My explanation

Lets imagine that you can save your object .LoggedIn(.playing) to UserDefaults. You want to save UserState's .LoggedIn case. There are 2 main questions:

  1. What should be saved to the storage? Your enum case does not have any value, there is nothing to save. You may think that it has associated value, it can be saved to the storage. So the 2nd question.

  2. (imagine that you saved it to storage) After you get saved value from storage, how are you going to determine what the case is it? You may think that it can be determined by the type of associated value, but what if you have lots of cases with the same associated value types? So the compiler does, it does not know what to do in this situation.

Your problem

You are trying to save the enum's case itself, but the saving to the UserDefaults does not convert your object by default. You can try to encode your .LoggedIn case to Data and then save the resulting data to UserDefaults. When you will need to get saved value you will get the data from storage and decode the enum's case from the data.

LowKostKustomz
  • 424
  • 4
  • 8