Saturday, June 2, 2018

Swift 4: getter and setter directly in a struct or class

Leave a Comment

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.

4 Answers

Answers 1

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

Answers 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.

Answers 3

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] 

Answers 4

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.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment