1
\$\begingroup\$

I made this generic class which helps me reading values from user defaults. Besides it abusing several UserDefaults... what do you see? Can I make some instances share a reference?

class DefaultValue<V> {
    var userDefaults = UserDefaults()
    let name : String
    let defValue : V

    init(name: String, defValue: V) {
        self.name = name
        self.defValue = defValue
    }

    func get() -> V {
        if userDefaults.object(forKey: name) == nil {
            return defValue
        }
        switch defValue {
        case is Int:
            return userDefaults.integer(forKey: name) as! V
        case is Bool:
            return userDefaults.bool(forKey: name) as! V
        case is String:
            return userDefaults.string(forKey: name) as! V
        default:
            return defValue
        }
    }

    func set(value: V) {
        userDefaults.set(value, forKey: name)
    }

    func reset() {
        userDefaults.set(defValue, forKey: name)
    }
}

Usage:

class MyDefaults {
    let LOGGED_IN = DefaultValue<Bool> (name: "logged_in", defValue: false)
    let USERNAME = DefaultValue<String> (name: "username", defValue: "")
}

var settings = MyDefaults()
if settings.LOGGED_IN.get() {
    settings.LOGGED_IN.set(value: true)
} else {
    settings.LOGGED_IN.reset()
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$ – Martin R Mar 12 '18 at 10:13
1
\$\begingroup\$

The forced casts as! V in the get() method might seem safe at first sight, since you do some type-checking before. But they are not: If a default value for some key exists, but is of the wrong type then your program can crash. This can for example happen if you change the type of some default value between program releases. You can easily reproduce the problem by setting the default value to an incompatible type before retrieving it with your class:

UserDefaults.standard.set([1, 2, 3], forKey: "username")

class MyDefaults {
    let USERNAME = DefaultValue<String> (name: "username", defValue: "Joe")
}

print(MyDefaults.USERNAME.get())

which crashes at

return userDefaults.string(forKey: name) as! V
// Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

This can be avoided with optional casting as? V, and then the entire get() method simplifies to

func get() -> V {
    return userDefaults.object(forKey: name) as? V ?? defValue
}

As a bonus, the code now works not only with integers, booleans and strings, but with all types (provided that they are “UserDefaults compatible”).


I do not see a reason to create separate UserDefaults instances, you can share the standard instance:

let userDefaults = UserDefaults.standard

The reset() method could simply remove the value instead of setting the default value:

func reset() {
    userDefaults.removeObject(forKey: name)
}

A possible advantage is that you can retrieve it with a different default value later.


I would use “default” as argument label in the init method:

init(name: String, default defValue: V) {
    self.name = name
    self.defValue = defValue
}

That looks nicer when called

let USERNAME = DefaultValue<String> (name: "username", default: "")

and also mimics the subscript(_:default:) of Swift dictionaries.

The set() method should have an empty argument label

func set(_ value: V) {
    userDefaults.set(value, forKey: name)
}

like, for example, set(_:forKey:).


If your intention of class MyDefaults is to avoid global variables and to provide a “namespace”: This can also be achieved with a case-less enum:

enum MyDefaults {
    static let USERNAME = DefaultValue<String>(name: "username", default: "")
}

MyDefaults.USERNAME.set("elcuco")
print(MyDefaults.USERNAME.get())

so that no instances have to be created.


Naming: According to the Swift API Design Guidelines:

Names of types and protocols are UpperCamelCase. Everything else is lowerCamelCase

so it should be

enum MyDefaults {
    static let loggedIn = DefaultValue<Bool> (name: "logged_in", default: false)
    static let userName = DefaultValue<String>(name: "username", default: "")
}

instead of LOGGED_IN and USERNAME.

\$\endgroup\$
  • \$\begingroup\$ Accepted all your suggestions, besides the UserDefaults.standard suggestion, which is not something I want to leave to the user of this .."library". See my new proposal. \$\endgroup\$ – elcuco Mar 12 '18 at 10:09
0
\$\begingroup\$

Lets add a new constructor:

init(name: String, default defValue: V, userDefaults: UserDefaults) {
    self.name = name
    self.defValue = defValue
    self.userDefaults = userDefaults
}

I don't like the idea of imposing a default settings on programmers using this library. New usage should look like this:

enum Settings {
    static let userDefaults = UserDefaults()
    static let loggedIn = DefaultValue<Bool> (name: "logged_in", default: false, userDefaults: userDefaults)
    static let userName = DefaultValue<String> (name: "username", default: "", userDefaults: userDefaults)
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.