How can I store a Swift enum value in NSUserDefaults

Solution 1:

Using rawValue for the enum is one way of using types that can be stored in NSUserDefaults, define your enum to use a rawValue. Raw values can be strings, characters, or any of the integer or floating-point number types :

enum Environment: String {
    case Production = "Prod"
    case Staging    = "Stg"
    case Dev        = "Dev"
}

You can also create an enum instance directly using the rawValue (which could come from NSUserDefaults) like:

let env = Environment(rawValue: "Dev")

You can extract the rawValue (String) from the enum object like this and then store it in NSUserDefaults if needed:

if let myEnv = env {
    println(myEnv.rawValue)
}


func saveEnvironment(environment : Environment){
    NSUserDefaults.standardUserDefaults().setObject(environment.rawValue, forKey: kSavedEnvironmentDefaultsKey)
}

Solution 2:

If you would like to save/read data from UserDefaults and separate some logic, you can do it in following way (Swift 3):

enum Environment: String {
    case Production
    case Staging
    case Dev
}

class UserDefaultsManager {

    static let shared = UserDefaultsManager()

    var environment: Environment? {
       get {
           guard let environment = UserDefaults.standard.value(forKey: kSavedEnvironmentDefaultsKey) as? String else {
               return nil
           }
           return Environment(rawValue: environment)
       }
       set(environment) {
           UserDefaults.standard.set(environment?.rawValue, forKey: kSavedEnvironmentDefaultsKey)
       }
    }
}

So saving data in UserDefaults will look this way:

UserDefaultsManager.shared.environment = Environment.Production

And reading data, saved in UserDefaults in this way:

if let environment = UserDefaultsManager.shared.environment {
    //you can do some magic with this variable
} else {
    print("environment data not saved in UserDefaults")
}

Solution 3:

Using Codable protocol

Extent Environment enum that conforms to Codable protocol to encode and decode values as Data.

enum Environment: String, Codable {
    case Production
    case Staging
    case Dev
}

A wrapper for UserDefaults:

struct UserDefaultsManager {
    static var userDefaults: UserDefaults = .standard
    
    static func set<T>(_ value: T, forKey: String) where T: Encodable {
        if let encoded = try? JSONEncoder().encode(value) {
            userDefaults.set(encoded, forKey: forKey)
        }
    }
    
    static func get<T>(forKey: String) -> T? where T: Decodable {
        guard let data = userDefaults.value(forKey: forKey) as? Data,
            let decodedData = try? JSONDecoder().decode(T.self, from: data)
            else { return nil }
        return decodedData
    }
}

Usage

// Set
let environment: Environment = .Production
UserDefaultsManager.set(environment, forKey: "environment")

// Get
let environment: Environment? = UserDefaultsManager.get(forKey: "environment")

Solution 4:

Swift 5.1 You can create a generic property wrapper, using Codable to transform values in and out the UserDefaults

extension UserDefaults {
    // let value: Value already set somewhere
    // UserDefaults.standard.set(newValue, forKey: "foo")
    //
    func set<T>(_ value: T, forKey: String) where T: Encodable {
        if let encoded = try? JSONEncoder().encode(value) {
            setValue(encoded, forKey: forKey)
        }
    }

    // let value: Value? = UserDefaults.standard.get(forKey: "foo")
    //
    func get<T>(forKey: String) -> T? where T: Decodable {
        guard let data = value(forKey: forKey) as? Data,
            let decodedData = try? JSONDecoder().decode(T.self, from: data)
            else { return nil }
        return decodedData
    }
}

@propertyWrapper
public struct UserDefaultsBacked<Value>: Equatable where Value: Equatable, Value: Codable {
    let key: String
    let defaultValue: Value
    var storage: UserDefaults = .standard

    public init(key: String, defaultValue: Value) {
        self.key = key
        self.defaultValue = defaultValue
    }

    // if the value is nil return defaultValue
    // if the value empty return defaultValue
    // otherwise return the value
    //
    public var wrappedValue: Value {
        get {
            let value: Value? = storage.get(forKey: key)
            if let stringValue = value as? String, stringValue.isEmpty {
                // for string values we want to equate nil with empty string as well
                return defaultValue
            }
            return value ?? defaultValue
        }
        set {
            storage.set(newValue, forKey: key)
            storage.synchronize()
        }
    }
}

// use it
struct AppState: Equatable {
    enum TabItem: String, Codable {
        case home
        case book
        case trips
        case account
    }
    var isAppReady = false

    @UserDefaultsBacked(key: "selectedTab", defaultValue: TabItem.home)
    var selectedTab
    // default value will be TabItem.home

    @UserDefaultsBacked(key: "selectedIndex", defaultValue: 33)
    var selectedIndex
    // default value will be 33

}

Solution 5:

Here is another alternative that can be be easily used with enums based on types (like String, Int etc) that can be stored by NSUserDefaults.

@propertyWrapper
struct StoredProperty<T: RawRepresentable> {
    let key: String
    let defaultValue: T

    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            guard let rawValue = UserDefaults.standard.object(forKey: key) as? T.RawValue, let value = T(rawValue: rawValue) else {
                 return defaultValue
            }
            return value
        }
        set {
            UserDefaults.standard.set(newValue.rawValue, forKey: key)
        }
    }
}

Example usage:

enum Environment: String {
    case Production
    case Staging
    case Dev
}

@StoredProperty("Environment", defaultValue: .Dev)
var storedProperty: Environment