SoFunction
Updated on 2025-04-12

iOS Data Persistence UserDefaults Packer usage detailed explanation

Use attribute wrappers to perfectly create UserDefaults wrapper

Imagine you have an app that wants to implement the automatic login function. You use UserDefaults to encapsulate the read and write logic about UserDefaults. You will use UserDefaults to keep track of the automatic login "On/Off" status and userName. You might encapsulate UserDefaults in the following way

struct AppData {
    private static let enableAutoLoginKey = "enable_auto_login_key"
    private static let usernameKey = "username_key"
    static var enableAutoLogin: Bool {
        get {
            return (forKey: enableAutoLoginKey)
        }
        set {
            (newValue, forKey: enableAutoLoginKey)
        }
    }
    static var username: String {
        get {
            return  
        }
        set {
            (newValueds, forKey: usernameKey)
        }
    }
}

Through the introduction of property wrappers in Swift 5.1, we can simplify the above code, as follows

struct AppData {
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var enableAutoLogin: Bool
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
}

Is this perfect? Let's watch

What is an attribute wrapper?

Before we get into the detailed discussion, let's take a quick look at what an attribute encapsulator is. Basically, an attribute encapsulator is a common data structure that intercepts read and write access to attributes, allowing custom behavior to be added during read and write attributes.

Can be passed through keywords@propertyWrapperTo declare an attribute wrapper. You want to have a string type attribute, and the console outputs it whenever this attribute is read and write. You can create a name calledPrintableThe property wrapper is as follows:

@propertyWrapper
struct Printable {
    private var value: String = ""
    var wrapperValue: String {
        get {
            print("get value:\(value)")
            return value
        }
        set {
            print("set value:\(newValue)")
            value = newValue
        }
    }
}

Through the above code, we can see that attribute encapsulation and otherstructSame. However, when defining an attribute encapsulator, there must be awrapppedValuewrapppedValue get setA code block is where you intercept and perform what you want. In this example, a code for printing status is added to output the values ​​of get and set

Next, let's take a look at how to use the Printable property wrapper

struct Company {
    @Printable static var name: String
}
 = "Adidas"

It should be noted that how we use it@Symbols to declare a "name" variable encapsulated with an attribute encapsulator. If you want to try to type the above code in Playground, you will see the following output:

Set Value: Adidas
Get Value: Adidas

What is a UserDefault Packer

After understanding what the property wrapper is and how it works, we are now ready to implement ourUserDefaultsPacketer. To sum up, our property wrapper needs to continuously track the "On/Off" status of automatic login and the username of the user. By using the concepts we discussed above, we can easilyPrintableThe attribute wrapper is converted into an attribute wrapper that reads and writes during a read and write operation.

import Foundation
@propertyWrapper
struct Storage {
    private let key: String
    private let defaultValue: String
    init(key: Stirng, defaultValue: String) {
         = key
         = defaultValue
    }
    var wrappedValue: String {
        get {
            return (forKey: key) ?? defaultValue
        }
        set {
            (newValue, forKey: key)
        }
    }
}

Here we name our property wrapperStorage. There are two attributes, one iskey, one isdefaultValuekeyWill beUserDefaultskeys for reading and writing, anddefaultValueThen asUserDefaultsReturn value when there is no value.

StorageOnce the property wrapper is ready, we can start implementing itUserDefaultsThe package is now. To be straightforward, we just need to create aStorageThe 'username' variable encapsulated by the property wrapper. It should be noted here that you can passkeyanddefaultValueTo initializeStorage

struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
}

After everything is ready,UserDefaultsThe package is ready to use

 = "swift-senpai"
print()

At the same time, let's addenableAutoLoginVariables to oursUserDefaultsIn the package

struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var username: Bool
}

At this time, the following two errors will be reported:

Cannot convert value of type ‘Bool’ to expected argument type ‘String’

Property type 'Bool' does not match that of lthe 'WrappedValue' property of its wrapper type 'Storage'

This is because our package only supportsStringtype. To resolve these two errors, we need to generalize our property wrapper

Generalize the property wrapper

We must change the property encapsulatorwrappedValueThe data type to perform generalization of the encapsulator willStringChange the type to genericT. Furthermore, we must use the common method fromUserDefaultsRead to updatewrappedValue getCode block

@propertyWrapper
struct Storage<T> {
    private let key: String
    private let defaultValue: T
    init(key: String, defaultValue: T) {
         = key
         = defaultValue
    }
    var wrappedValue: T {
        get {
            // Read value from UserDefaults
            return (forKey: key) as? T ?? defaultValue
        }
        set {
            // Set value to UserDefaults
            (newValue, forKey: key)
        }
    }
}

OK, with the general attribute wrapper, ourUserDefaultsThe encapsulator can store Bool-type data

// The UserDefaults wrapper
struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var enableAutoLogin: Bool
}
 = true
print()  // true

Store custom objects

The above operations are all used for basic data types. But what if we want to store custom objects? Next, let's take a look at how to makeUserDefaultsSupports storage of custom objects

The content here is very simple, we will store a custom object toUserDefaultsIn order to achieve this goal, we must transform itStorageType of property wrapperT, make it followCodableprotocol

Then, inwrappedValue``setWe will use the code blockJSONEncoderConvert custom objects to Data and write them toUserDefaultsmiddle. At the same time,wrappedValue``getIn the code block, we will useJSONDecoderTake fromUserDefaultsThe data read in it is converted into the corresponding data type. as follows:

@propertyWrapper
struct Storage<T: Codable> {
    private let key: String
    private let defaultValue: T
    init(key: String, defaultValue: T) {
         = key
         = defaultValue
    }
    var wrappedValue: T {
        get {
            // Read value from UserDefaults
            guard let data = (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(, from: data)
            return value ?? defaultValue
        }
        set {
            // Convert newValue to data
            let data = try? JSONEncoder().encode(newValue)
            // Set value to UserDefaults
            (data, forKey: key)
        }
    }
}

To let everyone see how to use the updatedStorageFor attribute wrapper, let's take a look at the next example. Imagine that you need to store user information returned by the server after the user logs in successfully. First, a struct holding the user information returned by the server is needed. This struct must be followedCodableProtocol, so that it can be converted into Data to store itUserDefaultsmiddle

struct User: Codable {
    var firstName: String
    var lastName: String
    var lastLogin: Date?
}

Next, inUserDefaultsDeclare a packagerUserObject

struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var enableAutoLogin: Bool
    // Declare a User object
    @Storage(key: "user_key", defaultValue: User(firstName: "", lastName: "", lastLogin: nil))
    static var user: User
}

nailed it,UserDefaultsThe encapsulator can now store custom objects

let johnWick = User(firstName: "John", lastName: "Wick", lastLogin: Date())
// Set custom object to UserDefaults wrapper
 = johnWick
print() // John
print() // Wick
print(!) // 2019-10-06 09:40:26 +0000

The above is the detailed explanation of the use of the iOS Data Persistence UserDefaults Package. For more information about iOS Data Persistence UserDefaults, please pay attention to my other related articles!