With due reference:
https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#user-defaults
We've started to use property wrappers for the UserDefaults, it works seamlessly with non-optional properties.
However, setting nil of an optional property crashes with:
[User Defaults] Attempt to set a non-property-list object as an NSUserDefaults/CFPreferences value for key "someKeyThatWeSet"
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object null for key "someKeyThatWeSet"'
The code below can be tested on Playground directly:
@propertyWrapper
struct C2AppProperty<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
struct C2User {
@C2AppProperty("userID", defaultValue: nil)
public static var publicUserID: String?
}
print(C2User.publicUserID)
C2User.publicUserID = "edusta"
print(C2User.publicUserID)
C2User.publicUserID = nil
print(C2User.publicUserID)
Expected:
nil
Optional<"edusta">
nil
Found:
nil
Optional<"edusta">
libc++abi.dylib: terminating with uncaught exception of type NSException
What I've tried so far:
set {
// Comparing non-optional value of type 'T' to nil always returns false.
if newValue == nil {
UserDefaults.standard.removeObject(forKey: combinedKey)
} else {
UserDefaults.standard.set(newValue, forKey: combinedKey)
}
}
What kind of a check is needed here to catch that newValue is nil
? Or an Optional<nil>
?
Property Wrappers. A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property. For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property.
You can create a Property Wrapper by defining a struct and marking it with the @propertyWrapper attribute. The attribute will require you to add a wrappedValue property to provide a return value on the implementation level.
Property wrappers were introduced in Swift 5.1 as a way to make it easier to reuse common programming patterns, but since then they have grown to work with local context, function and closure parameters, and more.
A property wrapper is a generic structure that encapsulates read and write access to the property and adds additional behavior to it. We use it if we need to constrain the available property values, add extra logic to the read/write access (like using databases or user defaults), or add some additional methods.
This code works for me:
@propertyWrapper
struct UserDefault<T> {
private let key: String
private let defaultValue: T
private let userDefaults: UserDefaults
init(_ key: String, defaultValue: T, userDefaults: UserDefaults = .standard) {
self.key = key
self.defaultValue = defaultValue
self.userDefaults = userDefaults
}
var wrappedValue: T {
get {
guard let value = userDefaults.object(forKey: key) else {
return defaultValue
}
return value as? T ?? defaultValue
}
set {
if let value = newValue as? OptionalProtocol, value.isNil() {
userDefaults.removeObject(forKey: key)
} else {
userDefaults.set(newValue, forKey: key)
}
}
}
}
fileprivate protocol OptionalProtocol {
func isNil() -> Bool
}
extension Optional : OptionalProtocol {
func isNil() -> Bool {
return self == nil
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With