23

I am trying to come up with a way to access UserDefaults using properties. However, I am stuck because I can't figure out how to make the properties dynamic, so to speak.

This is the generic idea, that API that I want:

class AppUserDefaults {
  var username = DefaultsKey<String>("username", defaultValue: "Unknown")
  var age = DefaultsKey<Int?>("age", defaultValue: nil)
}

let props = AppUserDefaults()
props.username = "bla"
print("username: \(props.username)")

But that (of course) doesn't work, since the type is DefaultsKey<String>, not String. So I have to add a value property to the DefaultsKey implementation just to add the getter and the setter, like this:

struct DefaultsKey<ValueType> {
  private let key: String
  private let defaultValue: ValueType

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

  var value: ValueType {
    get {
      let value = UserDefaults.standard.object(forKey: key)
      return value as? ValueType ?? defaultValue
    }
    set {
      UserDefaults.standard.setValue(newValue, forKey: key)
      UserDefaults.standard.synchronize()
    }
  }
}

and then use it like this:

let props = AppUserDefaults()
props.username.value = "bla"
print("username: \(props.username.value)")

But I find that rather ugly. I also tried a subscript method, but then you're still required to add [] instead of .value:

struct DefaultsKey<ValueType> {
  ...
  subscript() -> ValueType {
    get {
      let value = UserDefaults.standard.object(forKey: key)
      return value as? ValueType ?? defaultValue
    }
    set {
      UserDefaults.standard.setValue(newValue, forKey: key)
      UserDefaults.standard.synchronize()
    }
  }
}

let props = AppUserDefaults()
props.username[] = "bla"
print("username: \(props.username[])")

Basically what I want is that I can define the getter and the setter directly on DefaultsKey instead of having to go through that value property. Is this simply not possible with Swift? Is there another way to get the behaviour that I want, where properties defined on AppUserDefaults are "dynamic" and go through a getter and setter, without having to define it on the property declaration inside of AppUserDefaults?

I hope I am using the correct terms here and made the question clear for everyone.

Hamish
  • 78,605
  • 19
  • 187
  • 280
Kevin Renskers
  • 5,156
  • 4
  • 47
  • 95
  • 3
    This will become a lot easier in Swift 4.2 when you can use a `@dynamicMemberLookup` struct. – matt May 26 '18 at 22:32
  • 2
    True, but it'll lose a lot of type-certainty (not really type-safety, but you won't easily be able to restrict the keys at compile-time that way). I'm fairly certain that as written, this isn't possible; I'd say to use SwiftGen to get this (though personally, I'd accept `.value`; I use that all the time for observable things and it has the nice effect of letting you know "this isn't a trivial assignment.") – Rob Napier May 26 '18 at 22:42
  • Side-note, I'd make DefaultsKey a class and the properties "let" in AppUserDefaults. As written, it's possible to swap out one `DefaultsKey` for another, which isn't what you mean. (BTW, this problem can of course be solved really easily with a custom operator, but IMO that's much worse than `.value`) – Rob Napier May 26 '18 at 22:44
  • I've actually played around with a snapshot of 4.2 and `@dynamicMemberLookup` does not really make this possible either. It kinda works, but you can't have defined properties like `username` because those don't go through the `@dynamicMemberLookup` system. And you really want properties like `username` otherwise it's basically just stringly typed (accidentally using `usernamee` would not give an error). – Kevin Renskers May 29 '18 at 14:58
  • Please check [this answer](https://stackoverflow.com/a/50590993/9865234) of mine, which is using Enum keys – gorzki May 30 '18 at 21:17
  • Hey @MichalGorzalczany, that method is what we currently use in our project, but it's those repeated getters and setters for our 30 properties we want to get rid of :) – Kevin Renskers May 31 '18 at 11:47
  • @KevinRenskers Ok, now I fully understand your question and I am curious if someone will find the way. I'll let you know if i find sth by myself. – gorzki Jun 01 '18 at 09:39
  • I've found that [literal expressible](https://developer.apple.com/documentation/swift/initialization_with_literals) may help with setter, but still have no clue about getter so this is probably wrong direction – gorzki Jun 01 '18 at 14:47

4 Answers4

5

The only thing I can think of is this:

struct DefaultsKey<ValueType> {
  private let key: String
  private let defaultValue: ValueType

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

  var value: ValueType {
    get {
      let value = UserDefaults.standard.object(forKey: key)
      return value as? ValueType ?? defaultValue
    }
    set {
      UserDefaults.standard.setValue(newValue, forKey: key)
      UserDefaults.standard.synchronize()
    }
  }
}

class AppUserDefaults {
  private var _username = DefaultsKey<String>("username", defaultValue: "Unknown")
  var username: String {
    set {
      _username.value = newValue
    },
    get {
      return _username.value
    }
  }
}

let props = AppUserDefaults()
props.username = "bla"
print("username: \(props.username)")
Mihai Fratu
  • 7,579
  • 2
  • 37
  • 63
  • Thanks, that's is an interesting workaround for now. It's not ideal since every single property needs to have these getters and setters (and I have a project with about 40 userdefault keys), but it would still be a lot less code and boilerplate then what I am currently dealing with in this project :) – Kevin Renskers May 29 '18 at 10:48
2

Besides all proposed variants you can also define your custom operator for assigning value to DefaultsKey structure.

For that your DefaultsKey structure should look like this:

struct DefaultsKey<ValueType> {
    private let key: String
    private let defaultValue: ValueType

    public init(_ key: String, defaultValue: ValueType) {
        self.key = key
        self.defaultValue = defaultValue
    }
    public private(set) var value: ValueType {
        get {
            let value = UserDefaults.standard.object(forKey: key)
            return value as? ValueType ?? defaultValue
        }
        set {
            UserDefaults.standard.setValue(newValue, forKey: key)
        }
    }
}

Explanation for DefaultsKey block of code:

  1. private(set) var means that you can set value of this property only where you can access it with private access level (also you can write internal(set) or fileprivate(set) to be able to set it from internal and fileprivate access levels accordingly).

    You will need to set value property later. To access this value getter is defined as public (by writing public before private(set)).

  2. You do not need to use synchronize() method (" this method is unnecessary and shouldn't be used", reference: https://developer.apple.com/documentation/foundation/userdefaults/1414005-synchronize).

Now it's time to define custom operator (you can name it as you want, this is just for example):

infix operator <<<
extension DefaultsKey {
    static func <<<(left: inout DefaultsKey<ValueType>, right: ValueType) {
        left.value = right
    }
}

With this operator you couldn't set value of wrong type, so it's type-safe.

To test you can use a bit modified your code:

class AppUserDefaults {
    var username = DefaultsKey<String>("username", defaultValue: "Unknown")
    var age = DefaultsKey<Int?>("age", defaultValue: nil)
}

let props = AppUserDefaults()
props.username <<< "bla"
props.age <<< 21
props.username <<< "Yo man"
print("username: \(props.username.value)")
print("username: \(props.age.value)")

Hope it helps.

LowKostKustomz
  • 424
  • 4
  • 8
  • Thanks, but this is not the direction we want to go in with our project I'm afraid. Our team has no love for custom operators, me included :) – Kevin Renskers May 31 '18 at 11:44
0

There is a really good blog post from radex.io about Statically-typed NSUserDefaults that you might find useful, if I understand what you are trying to do.

I've updated it for the latest Swift (since we now have conditional conformance) and added a default value for my implementation, which I've set out below.

class DefaultsKeys {}

class DefaultsKey<T>: DefaultsKeys {
    let key: String
    let defaultResult: T

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

extension UserDefaults {
    subscript<T>(key: DefaultsKey<T>) -> T {
        get {
            return object(forKey: key.key) as? T ?? key.defaultResult
        }
        set {
            set(newValue, forKey: key.key)
        }
    }
}

// usage - setting up the keys
extension DefaultsKeys {
    static let baseCurrencyKey = DefaultsKey<String>("Base Currency", default: Locale.current.currencyCode!)
    static let archiveFrequencyKey = DefaultsKey<Int>("Archive Frequency", default: 30)
    static let usePasswordKey = DefaultsKey<Bool>("Use Password", default: false)
}

// usage - getting and setting a value
let uDefaults = UserDefaults.standard
uDefaults[.baseCurrencyKey] = "GBP"
let currency = uDefaults[.baseCurrencyKey]
closetCoder
  • 1,064
  • 10
  • 21
  • Yep I've seen this too, but I don't want to use it like `uDefaults[.baseCurrencyKey]`, but rather `uDefaults.baseCurrencyKey` :) – Kevin Renskers May 29 '18 at 10:46
0

Here is how we are doing that in my current project. It is similar to some other answers but I think even less boiler plate. Using isFirstLaunch as an example.

enum UserDafaultsKeys: String {
    case isFirstLaunch
}

extension UserDefaults {

    var isFirstLaunch: Bool {
        set {
            set(!newValue, forKey: UserDafaultsKeys.isFirstLaunch.rawValue)
            synchronize()
        }
        get { return !bool(forKey: UserDafaultsKeys.isFirstLaunch.rawValue) }
    }

}

It is then used like this.

UserDefaults.standard.isFirstLaunch = true

It has the benefits of not needing to learn a separate object to use and is very clear where the variable is stored.

Jeremiah
  • 1,471
  • 1
  • 13
  • 22
  • 1
    `synchronize()` is deprecated! Do not use it! [HERE](https://stackoverflow.com/a/50590993/9865234) is my similar answer with extended use case. – gorzki May 30 '18 at 21:01
  • This is exactly what we currently do in our project as well, but as we have over 30 properties, all these repeated getters and setters are getting a bit too much :) – Kevin Renskers May 31 '18 at 11:42
  • When you say "even less boilerplate", it's those setters and getters that are the boilerplate I want to get rid of. – Kevin Renskers May 31 '18 at 11:46
  • @KevinRenskers Ah, I understand what you are shooting for now. We have a super long list too, which is annoying but confined to one file at least. I prefer declaring them explicitly because it is clear which variables are available. This does also remove the `.value` which is nice. – Jeremiah May 31 '18 at 14:34
  • @MichalGorzalczany Thanks for pointing out `synchronize()` is deprecated. Its funny they commented it is deprecated but didn't actually mark it so. – Jeremiah May 31 '18 at 14:35
  • From Apple's documentation…  `synchronize()` _"… this method is unnecessary and shouldn't be used."_ https://developer.apple.com/documentation/foundation/userdefaults/1414005-synchronize – Ashley Mills Aug 09 '18 at 06:37