Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UserDefault property wrapper not saving values iOS versions below iOS 13

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?

like image 635
dayloosha Avatar asked Dec 24 '19 20:12

dayloosha


People also ask

What is a userdefaults wrapper and how do I use it?

Thus you create an UserDefaults wrapper to encapsulate the UserDefaults read and write logic. You will use the UserDefaults wrapper to keep track on the auto login “On” / “Off” status, as well as the user’s username. This is how your UserDefaults wrapper usually looks like:

Why does my Bool property not match my wrappedvalue property?

Property type ‘Bool’ does not match that of the ‘wrappedValue’ property of its wrapper type ‘Storage’ This is because our property wrapper currently only support String data type. In order to fix both errors, we will have to make our property wrapper generic.

What types of data can be stored in userdefaults?

With a generic property wrapper, our UserDefaults wrapper can now store a boolean value without any problem. At this point, our UserDefaults wrapper is able to store any basic data types such as String, Bool, Int, Float, Array, etc.

What is property wrapper in JavaScript?

Before we get into the details, let’s have a quick introduction on what is property wrapper. Basically, a property wrapper is a generic data structure that can intercept the property’s read / write access, thus enabling custom behaviour being added during the property’s read / write operation.


1 Answers

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.

like image 70
matt Avatar answered Oct 12 '22 23:10

matt