iOS數據持久化——UserDefaults

語言: CN / TW / HK

使用屬性封裝器來完美創建UserDefaults封裝器

想象一下,你有一個應用想實現自動登錄功能。你用UserDefaults封裝了關於UserDefaults的讀與寫邏輯。你會用UserDefaults封裝來保持對自動登錄”On/Off“狀態、userName的跟蹤。你可能會以下面這種方式來封裝UserDefaults ``` struct AppData { private static let enableAutoLoginKey = "enable_auto_login_key" private static let usernameKey = "username_key"

static var enableAutoLogin: Bool {
    get {
        return UserDefaults.standard.bool(forKey: enableAutoLoginKey)
    }
    set {
        UserDefaults.standard.set(newValue, forKey: enableAutoLoginKey)
    }
}

static var username: String {
    get {
        return UserDefaults.standard.string 
    }
    set {
        UserDefaults.standard.set(newValueds, forKey: usernameKey)
    }
}

} ``` 通過Swift5.1對於屬性封裝器的介紹,我們可以對上面的代碼進行精簡,如下

``` struct AppData { @Storage(key: "enable_auto_login_key", defaultValue: false) static var enableAutoLogin: Bool

@Storage(key: "username_key", defaultValue: "")
static var username: String

} ```

這樣就很完美了嗎?接着看

什麼是屬性封裝器?

在我們進入詳細討論之前,我們先快速地瞭解一下什麼是屬性封裝器 基本上來講,屬性封裝器是一種通用數據結構,可以攔截屬性的讀寫訪問,從而允許在屬性的讀寫期間添加自定義行為。

可以通過關鍵字@propertyWrapper來聲明一個屬性封裝器。你想要有一個字符串類型的屬性,每當這個屬性被進行讀寫操作的時候,控制枱就會輸出。你可以創建一個名為Printable的屬性封裝器,如下: ``` @propertyWrapper struct Printable { private var value: String = ""

var wrapperValue: String {
    get {
        print("get value:\(value)")
        return value
    }
    set {
        print("set value:\(newValue)")
        value = newValue
    }
}

} `` 通過上述代碼我們可以看出,屬性封裝跟其他struct一樣。然而,當定義一個屬性封裝器的時候,必須要有一個wrapppedValuewrapppedValuegetset`代碼塊就是攔截和執行你想要的操作的地方。在這個例子中,添加了打印狀態的代碼來輸出get和set的值

接下來,我們看看,如何使用Printable屬性封裝器 struct Company { @Printable static var name: String } Company.name = "Adidas" Company.name 需要注意的是,我們如何使用@符號來聲明一個用屬性封裝器封裝的”name“變量。如果你想要在Playground中嘗試敲出上述代碼的話,你會看到以下輸出: Set Value: Adidas Get Value: Adidas

什麼是UserDefault封裝器

在理解了什麼是屬性封裝器以及它是如何工作的之後,我們現在開始準備實現我們的UserDefaults封裝器。總結一下,我們的屬性封裝器需要持續跟蹤自動登錄的”On/Off“狀態以及用户的username。 通過使用我們上述討論的概念,我們可以很輕鬆的將Printable屬性封裝器轉化為在讀寫操作期間進行讀寫的屬性封裝器。

``` import Foundation @propertyWrapper struct Storage { private let key: String private let defaultValue: String

init(key: Stirng, defaultValue: String) {
    self.key = key
    self.defaultValue = defaultValue
}

var wrappedValue: String {
    get {
        return UserDefaults.standard.string(forKey: key) ?? defaultValue
    }

    set {
        UserDefaults.standard.set(newValue, forKey: key)
    }
}

} `` 在這裏,我們將我們的屬性封裝器命名為Storage。有兩個屬性,一個是key,一個是defaultValuekey將作為UserDefaults讀寫時的鍵,而defaultValue則作為UserDefaults`無值時候的返回值。

Storage屬性封裝器準備就緒後,我們就可以開始實現UserDefaults封裝器了。直截了當,我們只需要創建一個被Storage屬性封裝器封裝的‘username’變量。這裏要注意的是,你可以通過keydefaultValue來初始化Storage

struct AppData { @Storage(key: "username_key", defaultValue: "") static var username: String } 一切就緒之後,UserDefaults封裝器就可以使用了 ``` AppData.username = "swift-senpai"

print(AppData.username) 同時,我們來添加`enableAutoLogin`變量到我們的`UserDefaults`封裝器中 struct AppData { @Storage(key: "username_key", defaultValue: "") static var username: String

@Storage(key: "enable_auto_login_key", defaultValue: false)
static var username: Bool

} ``` 這個時候,會報下面兩種錯誤: - 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'

這是因為我們的封裝器目前只支持String類型。想要解決這兩個錯誤,我們需要將我們的屬性封裝器進行通用化處理

將屬性封裝器進行通用化處理

我們必須改變屬性封裝器的wrappedValue的數據類型來進行封裝器的通用化處理,將String類型改成泛型T。進而,我們必須使用通用方式從UserDefaults讀取來更新wrappedValue get代碼塊

``` @propertyWrapper struct Storage { private let key: String private let defaultValue: T

init(key: String, defaultValue: T) {
    self.key = key
    self.defaultValue = defaultValue
}

var wrappedValue: T {
    get {
        // Read value from UserDefaults
        return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
    }
    set {
        // Set value to UserDefaults
        UserDefaults.standard.set(newValue, forKey: key)
    }
}

} 好,有了通用屬性封裝器之後,我們的`UserDefaults`封裝器就可以存儲Bool類型的數據了 // 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

}

AppData.enableAutoLogin = true print(AppData.enableAutoLogin) // true ```

存儲自定義對象

上面的操作都是用來基本數據類型的。但是如果我們想要存儲自定義對象呢?接下來我們一起看看,如何能讓UserDefaults支持自定義對象的存儲

這裏的內容很簡單,我們將會存儲一個自定義對象到UserDefaults中,為了達到這個目的,我們必須改造一下Storage屬性封裝器的類型T,使其遵循Codable協議

然後,在wrappedValue``set代碼塊中我們將使用JSONEncoder把自定義對象轉化為Data,並將其寫入UserDefaults中。同時,在wrappedValue``get代碼塊中,我們將使用JSONDecoder把從UserDefaults中讀取的數據轉化成對應的數據類型。 如下: ``` @propertyWrapper struct Storage { private let key: String private let defaultValue: T

init(key: String, defaultValue: T) {
    self.key = key
    self.defaultValue = defaultValue
}

var wrappedValue: T {
    get {
        // Read value from UserDefaults
        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)

        // Set value to UserDefaults
        UserDefaults.standard.set(data, forKey: key)
    }
}

} ```

為了讓大家看到如何使用更新後的Storage屬性封裝器,我們來看一下接下來的例子。 想象一下,你需要存儲用户登錄成功後服務端返回的用户信息。首先,需要一個持有服務端返回的用户信息的struct。這個struct必須遵循Codable協議,以至於他能被轉化為Data存儲到UserDefaults

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

接下來,在UserDefaults封裝器中聲明一個User對象 ``` 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

} ```

搞定了,UserDefaults封裝器現在可以存儲自定義對象了

``` let johnWick = User(firstName: "John", lastName: "Wick", lastLogin: Date())

// Set custom object to UserDefaults wrapper AppData.user = johnWick

print(AppData.user.firstName) // John print(AppData.user.lastName) // Wick print(AppData.user.lastLogin!) // 2019-10-06 09:40:26 +0000 ```

感謝大家的閲讀,給個讚唄😊