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:
private(set) var
means that you can set value of this property only where you can access it withprivate
access level (also you can writeinternal(set)
orfileprivate(set)
to be able to set it frominternal
andfileprivate
access levels accordingly).You will need to set
value
property later. To access this value getter is defined aspublic
(by writingpublic
beforeprivate(set)
).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.
0 comments:
Post a Comment