Swift 5.1, Xcode 11.2
Skip the boring: Solution for property with Optional type | Improved solution form proposal | UPDATE: Conclusions after using in practice
Property wrapper is the new feature in Swift 5.1. There are plenty of articles covering the topic of using this feature for many purposes. One of them is wrapping property around UserDefaults, which means using UserDefaults (UserDefults.standard
in most cases, but this is not the only possibility) instead of backing variable for the property.
I do not want to duplicate the topic when there are so many other places where this is described very well:
- Proposal - where everything started
- NSHipster - Swift Property Wrappers
- SwiftLee - Property wrappers to remove boilerplate code in Swift
- and others.
However, everyone is focusing only on the simplest cases, but no one is speaking about the issues.
What if...
Let's take some example implementation of the property wrapper:
@propertyWrapper public struct UserDefault<T> {
public let key: String
public let defaultValue: T
public var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
All seams to look good and it works in the most common cases like:
@UserDefault(key: "some_flag", defaultValue: false)
public var someFlag: Bool
Maybe sometimes there will be a need to set also initial value despite the fact that we have the No, it's a terrible idea, more about it further:defaultValue
, then we have to add following two initializers
public init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
// !!!: there was no such initializer in the proposal, it's my terrible idea
public init(wrappedValue: T, key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
self.wrappedValue = wrappedValue
}
now we can could use it also like this:
@UserDefault(key: "some_flag", defaultValue: false)
public var someFlag: Bool ~~= true~~
But what if the type of the property will be Optional value type?
This generic struct might be adopted in such case. What happens then?
To find out I'm going to use the UserDefaultPropertyWrapper.playground available on GitHub, where:
1. Property wrapper is modified like this:
@propertyWrapper public struct UserDefault<T> {
public let key: String
public let defaultValue: T
public var wrappedValue: T {
get {
let udObject = UserDefaults.standard.object(forKey: key)
let udValue = udObject as? T
let value = udValue ?? defaultValue
print("get UDObject:", udObject as Any, "UDValue:", udValue as Any, "defaultValue:", defaultValue, "returned value:", value)
return value
}
set {
print("set UDValue:", newValue as Any, "for key:", key)
UserDefaults.standard.set(newValue as Any, forKey: key)
}
}
}
2. Property is defined as the following:
class Some {
@UserDefault(key: "optional_flag", defaultValue: false)
public var optionalFlag: Bool?
}
3. Let's test getter and setter:
let object = Some()
object.optionalFlag
object.optionalFlag = true
object.optionalFlag
object.optionalFlag = nil
Console:
Property Init with key: 'optional_flag', defaultValue: 'Optional(false)'
get UDObject: nil UDValue: Optional(nil) defaultValue: Optional(false) returned value: nil
set UDValue: Optional(true) for key: optional_flag
get UDObject: Optional(1) UDValue: Optional(Optional(true)) defaultValue: Optional(false) returned value: Optional(true)
set UDValue: nil for key: optional_flag
libc++abi.dylib: terminating with uncaught exception of type NSException
This is quite tricky.
First get a fix on the fact that print(...)
method used to print console logs unwraps values, so for Optional<Bool>(nil)
or if you will Optional<Bool>.none
it will print nil
, and for Optional<Bool?>(value)
will print Optional(value)
.
As we can see in console log line 2: UDValue is not nil
but in fact Optional(nil)
which means that it is wrapped twice. It is even more visible in line 4 of the console log above.
It can be simply confirmed by printing:
print(Optional<Bool>(nil)) // nil
print(Optional<Bool?>.some(Optional<Bool>(nil))) // Optional(nil)
print(Optional<Bool?>.some(Optional<Bool>(true)))// Optional(Optional(true))
Why is that? Because we use the conditional cast on Optional type: as? T
where T
is Bool?
so we do something like this as? Bool?
which returns Bool??
So what is happening?
1. The getter
When stored value is nil
(simply not set in UserDefaults
) we have:
let defaultValue: Bool? = Optional<Bool>.some(false)
let udObject: Bool? = Optional<Bool>.none
let udValue: Bool?? = (Optional<Bool>.none as? Bool?) // Optional<Bool?>.some(Optional<Bool>.none)
let value: Bool? = Optional<Bool?>.some(Optional<Bool>.none) ?? defaultValue // expression returns `Optional<Bool>.none` not `defaultValue`
print("get UDObject:", udObject, "UDValue:", udValue, "defaultValue:", defaultValue, "returned value:", value)
So what we see here?
- getter will never return
defaultValue
if the type of the propertyT
isOptional
type - returned value will be
nil
which is a valid value forBool?
type
The same case is with concrete value
As long as this issue causes only some unexpected behavior, there is a much worse issue.
2. The setter
When we try to set nil
to the property with Optional type the setter crashes:
Long story short this is also caused by the Optional in the Optional, and the same same error occurs if you write:
UserDefaults.standard.set(Optional(Optional<Bool>(nil)), forKey: "optional_flag")
What we can do?
1. Solution for the property with Optional type. ↩
Simple option and maybe preferred by some people is to use separate wrapper for optional values.
@propertyWrapper
public struct OptionalUserDefault<T> {
public let key: String
public var wrappedValue: T? {
get {
return UserDefaults.standard.object(forKey: key) as? T
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
The solution is not so bad because:
- distinguishes the case where the value is optional
- there is no need to define
defautlValue
because it is not needed since we expect that the value might not be there.
However, it does not guarantee that nobody will use @UserDefault(key:defaultValue:)
attribute to a property with Optional type.
That's why we should fix the proposal code.
2. Improved solution form proposal. ↩
There is another solution that allows us to use one wrapper for every mentioned case or at least make it safer.
@propertyWrapper
public struct UserDefault<T> {
public let key: String
public let defaultValue: T
public var wrappedValue: T {
get {
let udValue = UserDefaults.standard.object(forKey: key) as? T
switch (udValue as Any) {
case Optional<Any>.some(let value):
return value as! T
case Optional<Any>.none:
return defaultValue
default:
return udValue ?? defaultValue
}
}
set {
switch (newValue as Any) {
case Optional<Any>.some(let value):
UserDefaults.standard.set(value, forKey: key)
case Optional<Any>.none:
UserDefaults.standard.removeObject(forKey: key)
default:
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
}
UPDATE: Conclusions after using in practice. ↩
My experience showed that there still is something to improve here.
1. The initial value is a very bad idea
I mean allowing to set initial value during initialization like this:
@UserDefault(key: "some_flag", defaultValue: false)
public var someFlag: Bool = true
thanks to the initializer:
/// Initializer allowing to set initialValue of the property
public init(wrappedValue: T, key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
self.wrappedValue = wrappedValue
}
The initial value may cause more harm than profits. If we have a property with an initial value, it will override any value previously stored in the UserDefaults under the given key.
Let say we have a user defaults key "some_key"
If somewher in the code you did set value for the key UserDefaults.standard.set(true, forKey: "some_key")
And you have the following class
class SomeSettings {
@UserDefault(key: "some_key", defaultValue: false)
var someFlag: Bool = false
}
Then every time the class instance is created, it will cause overriding the previously set value in the UserDefaults with false
.
That is why we should drop the idea of the initial value in case of property wrapper for storages like UserDefaults or Keychain.
As long as we have a default value, it is not needed anyway.
2. It's good to restrict allowed types to the set of types supported by Property List format.
Since we know which types are acceptable by the Property List, we can restrict our structs to be used only with the values of selected types.
To do so, we need:
- declare working protocol
PlistCompatible
. -
make compatible types conforming to the protocol.
public protocol PlistCompatible {} // MARK: - UserDefaults Compatibile Types extension String: PlistCompatible {} extension Int: PlistCompatible {} extension Double: PlistCompatible {} extension Float: PlistCompatible {} extension Bool: PlistCompatible {} extension Date: PlistCompatible {} extension Data: PlistCompatible {} extension Array: PlistCompatible where Element: PlistCompatible {} extension Dictionary: PlistCompatible where Key: PlistCompatible, Value: PlistCompatible {}
-
force the generic type
T
to conform to the protocol
@propertyWrapper public struct UserDefault<T: PlistCompatible> {
Our Wrappers are looking like this:
@propertyWrapper public struct UserDefault<T: PlistCompatible> { public let key: String public let defaultValue: T public var wrappedValue: T { get { return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } } @propertyWrapper public struct OptionalUserDefault<T: PlistCompatible> { public let key: String public var wrappedValue: T? { get { return UserDefaults.standard.object(forKey: key) as? T } set { UserDefaults.standard.set(newValue, forKey: key) } } }
3. It would be handy to be able to utilize the wrapper for custom types
To hadle this case we need to write separate wrappers. These will be storing value of type T
conforming to RawRepresentable
with restriction that its RawValue
is PlistCompatible
. Our wrapper requires simple modifications:
@propertyWrapper
public struct WrappedUserDefault<T: RawRepresentable> where T.RawValue: PlistCompatible {
// ...
}
So we will need the following two definitions:
@propertyWrapper
public struct WrappedUserDefault<T: RawRepresentable> where T.RawValue: PlistCompatible {
public let key: String
public let defaultValue: T
public var wrappedValue: T {
get {
guard let value = UserDefaults.standard.object(forKey: key) as? T.RawValue else {
return defaultValue
}
return T.init(rawValue: value) ?? defaultValue
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: key)
}
}
}
@propertyWrapper
public struct OptionalWrappedUserDefault<T: RawRepresentable> where T.RawValue: PlistCompatible {
public let key: String
public var wrappedValue: T? {
get {
guard let value = UserDefaults.standard.object(forKey: key) as? T.RawValue else {
return nil
}
return T.init(rawValue: value)
}
set {
UserDefaults.standard.set(newValue?.rawValue, forKey: key)
}
}
}
Now the only thing we need is to make our custom type to conform to RawRepresentable
protocol.
4. What if the key depends on something?
I had the case when the key was depending on some value provided in init(id: String)
. Key would look like account[\(id)].someFlag
In this case, I needed to set the property key
during the initialization of the class and its properties. This is very interesting and possible to implement case but also not so common so since no one is reading... :) Maybe another time.
All files and code are available on the GitHub and free to use. Enjoy.
Top comments (1)
Thanks for the posting. I struggle to find the solution to get an optional value when setting defaultValue. 👍