I am using a property wrapper to save my User Default values. On iOS 13 devices, this solution works great. However on iOS 11 and iOS 12, the values are not being saved into User Defaults. I read that property wrappers are backwards compatible so I don't know why this would not work on older iOS versions.
This is the property wrapper:
@propertyWrapper
struct UserDefaultWrapper<T: Codable> {
private let key: String
private let defaultValue: T
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
guard let data = UserDefaults.standard.object(forKey: key) as? Data else {
// Return defaultValue when no data in UserDefaults
return defaultValue
}
// Convert data to the desire data type
let value = try? JSONDecoder().decode(T.self, from: data)
return value ?? defaultValue
}
set {
// Convert newValue to data
let data = try? JSONEncoder().encode(newValue)
UserDefaults.standard.set(data, forKey: key)
UserDefaults.standard.synchronize()
}
}
}
struct UserDefault {
@UserDefaultWrapper(key: "userIsSignedIn", defaultValue: false)
static var isSignedIn: Bool
}
I can then set the value like this:
UserDefault.isSignedIn = true
Am I using the property wrapper wrong? Is anyone else running into issues with property wrappers on older iOS versions?
Nothing to do with property wrappers! The problem is that in iOS 12 and before, a simple value like a Bool (or String, etc.), though Codable as a property of a Codable struct (for example), cannot itself be JSON encoded. The error (which you are throwing away) is quite clear about this:
Top-level Bool encoded as number JSON fragment.
To see this, just run this code:
do {
_ = try JSONEncoder().encode(false)
print("succeeded")
} catch {
print(error)
}
On iOS 12, we get the error. On iOS 13, we get "succeeded"
.
But if we wrap our Bool (or String, etc.) in a Codable struct, all is well:
struct S : Codable { let prop : Bool }
do {
_ = try JSONEncoder().encode(S(prop:false))
print("succeeded")
} catch {
print(error)
}
That works fine on both iOS 12 and iOS 13.
And that fact suggests a solution! Redefine your property wrapper so that it wraps its value in a generic Wrapper struct:
struct UserDefaultWrapper<T: Codable> {
struct Wrapper<T> : Codable where T : Codable {
let wrapped : T
}
private let key: String
private let defaultValue: T
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
guard let data = UserDefaults.standard.object(forKey: key) as? Data
else { return defaultValue }
let value = try? JSONDecoder().decode(Wrapper<T>.self, from: data)
return value?.wrapped ?? defaultValue
}
set {
do {
let data = try JSONEncoder().encode(Wrapper(wrapped:newValue))
UserDefaults.standard.set(data, forKey: key)
} catch {
print(error)
}
}
}
}
Now it works on iOS 12 and iOS 13.
By the way, I actually think you would do better to save as a property list rather than JSON. But that makes no difference to the question generally. You can’t encode a bare Bool as a property list either. You’d still need the Wrapper approach.