3

How would one implement a struct that manages UserDefaults mappings in Swift? Right now I have some computed properties a, b, c, d of different types and corresponding keys that look like this:

enum UserDefaultsKeys {
    a_key
    b_key
    ...
}
var a: String {
    get { UserDefaults.standard.string(forKey: UserDefaultsKeys.a_key.rawValue) }
    set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.a_key.rawValue) }
}
var b: Int {
    get { UserDefaults.standard.integer(forKey: UserDefaultsKeys.b_key.rawValue) }
    set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.b_key.rawValue) }
}
...

What I would like to achieve instead is to implement a struct that has a key of type String and a generic type value. The value get function should - depending on its type - chose wether to use UserDefaults.standard.string or UserDefaults.standard.integer so that I can just create a DefaultsVar with some key and everything else is managed automatically for me. What I have so far is this:

struct DefaultsVar<T> {
    let key: String
    var value: T {
        get {
            switch self {
                case is String: return UserDefaults.standard.string(forKey: key) as! T
                case is Int: return UserDefaults.standard.integer(forKey: key) as! T
                default: return UserDefaults.standard.float(forKey: key) as! T
            }
        }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}

I get the following error: "Cast from DefaultsVar to unrelated Type 'String' always fails. I am completely new to swift (and relatively new to programming) and don't really understand how to implement this the right way - or if this is even an approach that would be considered a good practice. Could someone please shine some light on this?

Thank you in advance!

yelinek
  • 43
  • 5
  • 2
    What is the benefit? The setter of `UserDefaults` **is** already generic and there are only a few supported types. – vadian Oct 27 '20 at 15:10
  • I thought it might prevent some boilerplate code for all the variables. – yelinek Oct 27 '20 at 15:16

1 Answers1

4

You can use a custom

Property wrapper:

@propertyWrapper
struct UserDefaultStorage<T: Codable> {
    private let key: String
    private let defaultValue: T

    private let userDefaults: UserDefaults

    init(key: String, default: T, store: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = `default`
        self.userDefaults = store
    }

    var wrappedValue: T {
        get {
            guard let data = userDefaults.data(forKey: key) else {
                return defaultValue
            }
            let value = try? JSONDecoder().decode(T.self, from: data)
            return value ?? defaultValue
        }
        set {
            let data = try? JSONEncoder().encode(newValue)
            userDefaults.set(data, forKey: key)
        }
    }
}

This wrapper can store/restore any kind of codable into/from the user defaults.

Usage

@UserDefaultStorage(key: "myCustomKey", default: 0)
var myValue: Int

iOS 14

SwiftUI has a similar wrapper (only for iOS 14) called @AppStorage and it can be used as a state. The advantage of using this is that it can be used directly as a State. But it requires SwiftUI and it only works from the iOS 14.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • There is no need to cast from `Any` to `Data`. `UserDefaults` has a specific method exactly to retrieve data from it. `data(forKey:)`. – Leo Dabus Oct 27 '20 at 15:41
  • Yeah, you are right @LeoDabus. I should update my gist :) Thanks – Mojtaba Hosseini Oct 27 '20 at 15:44
  • Great! Thank you for your answer! One question though: is it really preferable to return a default value (thus hiding mistakes that might have been made) instead of throwing an error if no value can be returned? – yelinek Oct 27 '20 at 15:49
  • The class itself always returns a default value. By the way, we are talking about user **Defaults** right? ;) – Mojtaba Hosseini Oct 27 '20 at 15:51
  • 1
    You're welcome. Don't forget the **upvote** if it was helpful and **accept** if it was the answer to you ;) – Mojtaba Hosseini Oct 27 '20 at 15:57
  • @MojtabaHosseini `return (try? JSONDecoder().decode(T.self, from: data)) ?? defaultValue` Btw you can move the decoding part to the guard as well – Leo Dabus Oct 27 '20 at 16:02
  • `guard let data = UserDefaults.standard.data(forKey: key), let value = try? JSONDecoder().decode(T.self, from: data) else { return defaultValue }` `return value` – Leo Dabus Oct 27 '20 at 16:05
  • I'm not a fan of writing everything in a single line. I tried to separate parts of that, so anyone can raise assertions for example ‍♂️. Thanks for your care :) – Mojtaba Hosseini Oct 27 '20 at 16:09