Search code examples
iosswiftgenericsnsuserdefaults

Property Wrappers With Generic (Optional) User Defaults


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>?


Solution

  • 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
        }
    }