如何将符合Codable协议的枚举保存到UserDefaults中?

5

回复我之前的问题

我已经成功让我的枚举符合Codable协议,实现了init()和encode()方法,看起来它可以正常工作。

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

我的问题是我不知道如何将它保存到UserDefaults中。如果我像现在这样保存,当运行应用程序时会出现以下错误:
[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]

以上示例中的 encoded 是什么? - Rob Napier
抱歉,应该在那里标明状态。这是一个类型问题。 - Adrian
@Kobe:请注意我在你的问题中所做的代码更改。无论如何,解码器都会抛出错误。 - djromero
请注意,使用Decodable协议解析JSON时,您可以抛出DecodingError而不是创建自己的错误类型。 https://dev59.com/SFYO5IYBdhLWcg3wDNQb#46458771 - Leo Dabus
3个回答

4

来自 UserDefaults 参考文档:

默认对象必须是属性列表 —— 也就是说,它必须是 NSData、NSString、NSNumber、NSDate、NSArray 或 NSDictionary 的实例(或者针对集合类型,是上述类型的组合)。如果你想存储其他类型的对象,通常应该将其归档以创建 NSData 实例。

因此,您应该手动编码 state 并将其作为数据存储:

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

然后,要将其读回来:
guard let data = UserDefaults.standard.data(forKey: "state") else { fatalError() }
if let saved = try? JSONDecoder().decode(UserState.self, from: data) {
    ...
}

不要使用value(forKey:)方法,UserDefaults有特定的方法来加载数据类型值,在这种情况下是data(forKey:),因此无需从Any?强制转换为Data,你只需要解包它的值。guard let data = UserDefaults.standard.data(forKey: "state") else { - Leo Dabus
@LeoDabus 谢谢。我完全忘记了那些特定的方法,我已经更新了答案。 - djromero
@LeoDabus 这取决于 OP(也许 UserState 值绑定到了一个 UISwitch 状态)。无论如何,这对我的答案来说都是不相关的,更相关的是在原始问题中作为评论,我个人认为。 - djromero

2

Swift 4+。具有普通 情况 和带有关联 情况Enum

  1. Enum 的代码:
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. 将数据写入UserDefaults:
let one1 = Enumeration.two(442)

if let encoded = try? encoder.encode(one1) {
    UserDefaults.standard.set(encoded, forKey: "savedEnumValue")
}
  1. 从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)"
    }
}

0

枚举需要有rawValue才能作为Codable对象进行保存。由于您的枚举案例具有相关联的值,因此它们不能具有rawValue。

带有良好解释的文章

我的解释

让我们想象一下,您可以将您的对象.LoggedIn(.playing)保存到UserDefaults中。您想要保存UserState.LoggedIn案例。有2个主要问题:

  1. 应该保存什么到存储器?您的枚举情况没有任何值,没有东西可以保存。您可能认为它具有相关联的值,可以保存到存储器中。所以第二个问题。

  2. (假设您已将其保存到存储器中)在从存储器中获取保存的值之后,您将如何确定案例是什么?您可能认为可以通过关联值的类型来确定它,但是如果您有许多带有相同关联值类型的案例怎么办?编译器就会出现这种情况。它不知道该怎么办。

你的问题

你试图保存枚举的情况本身,但是默认情况下,将对象保存到UserDefaults时不会进行转换。你可以尝试将.LoggedIn情况编码为Data,然后将生成的数据保存到UserDefaults中。当你需要获取保存的值时,从存储中获取数据并从数据中解码枚举的情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接