iOS數據持久化——UserDefaults
使用屬性封裝器來完美創建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一樣。然而,當定義一個屬性封裝器的時候,必須要有一個
wrapppedValue。
wrapppedValueget
set`代碼塊就是攔截和執行你想要的操作的地方。在這個例子中,添加了打印狀態的代碼來輸出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,一個是
defaultValue。
key將作為
UserDefaults讀寫時的鍵,而
defaultValue則作為
UserDefaults`無值時候的返回值。
Storage
屬性封裝器準備就緒後,我們就可以開始實現UserDefaults
封裝器了。直截了當,我們只需要創建一個被Storage
屬性封裝器封裝的‘username’變量。這裏要注意的是,你可以通過key
和defaultValue
來初始化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
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
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 ```
感謝大家的閲讀,給個讚唄😊