原文在這里
納尼? 如此簡單的 UserDefaults 怎么去優(yōu)雅的使用? 這么簡單的還能玩出花來? 沒毛病吧?
嗯, 沒毛病!
Objective-C 中的 NSUserDefaults 我們并不陌生, 通常作為數(shù)據(jù)持久化的一種方式, 一般用來存儲用戶信息和基礎(chǔ)配置信息. Swift 中使用 UserDefaults 來替代 NSUserDefaults, 兩者的使用基本相同.
let defaults = UserDefaults.standard
defaults.set(123, forKey: "defaultKey")
defaults.integer(forKey: "defaultKey")
Objective-C中需要調(diào)用 synchronize 方法進(jìn)行同步, 但是在Swift中已經(jīng)廢棄了該方法, 所以不需要手動去調(diào)用.
-synchronize is deprecated and will be marked with the NS_DEPRECATED macro in a future release.
上面的用法是最基本的用法, 也是我們平常開發(fā)中使用頻率最高的用法, 但也是最危險的用法, 為什么呢?
- 在應(yīng)用內(nèi)部我們可以隨意地覆蓋和刪除存儲的值, 直接使用字符串來作為存儲數(shù)據(jù)的 key 是非常危險的, 容易導(dǎo)致存數(shù)據(jù)時使用的 key 和取數(shù)據(jù)的時候使用的 key 不一致.
- UserDefaults.standard 是一個全局的單例, 如果需要存儲賬戶信息(AccountInfo), 配置信息(SettingInfo), 此時按照最基本的使用方式, 簡單的使用 key 來存取數(shù)據(jù), 那么 key 值會隨著存儲的數(shù)據(jù)越來越多, 到時候不管是新接手的小伙伴還是我們自己都很難明白每個 key 值對應(yīng)的意義. 也就是說我們不能根據(jù)方法調(diào)用的上下文明確知道我存取數(shù)據(jù)的具體含義, 代碼的可讀性和可維護(hù)性就不高.所以我們要利用 Swift 強(qiáng)大的靈活性來讓我們使用 UserDefaults 存取數(shù)據(jù)的時候更加便捷和安全.
所以要想把 UserDefaults 玩出花來就得解決下面兩個問題:
一致性
上下文
一致性
使用 UserDefaults 存取數(shù)據(jù)時使用的 key 值不同就會導(dǎo)致存在一致性問題. 原因就在于通常我們在存取數(shù)據(jù)的時候, 手動鍵入 key 或者復(fù)制粘貼 key 可能會出錯, 輸入的時候也很麻煩. 那我們的目的就比較明確了, 就是為了讓存取的 key 一致, 即使改了其中一個另外一個也隨之更改.
解決辦法:
- 常量保存
- 分組存儲
常量保存字符串
既然涉及到兩個重復(fù)使用的字符串, 很容易就想到用常量保存字符串, 只有在初始化的時候設(shè)置 key 值, 存取的時候拿來用即可, 簡單粗暴的方式.
let defaultStand = UserDefaults.standard
let defaultKey = "defaultKey"
defaultStand.set(123, forKey: defaultKey)
defaultStand.integer(forKey: defaultKey)
是不是感覺有點換湯不換藥? 上面使用常量存儲 key 值, 雖然能夠保證存取的時候 key 值相同, 但是在設(shè)置 key 值的時候稍顯麻煩.
最重要的一點就是如果需要存很多賬戶信息或者配置信息的時候, 按照這種方式都寫在同一處地方就稍微欠妥, 比如下面這個場景, 在 app 啟動后, 需要存儲用戶信息和登錄信息, 用戶信息里面包含: userName, avatar, password, gender等, 登錄信息里包含: token, userId, timeStamp等等, 也就說需要存兩類不同的信息, 那么此時這種方式就不合時宜了, 我們就會想辦法把同類的信息歸為一組, 進(jìn)行分組存取.
分組存儲
分組存儲 key 可以把存儲數(shù)據(jù)按不同類別區(qū)分開, 代碼的可讀性和可維護(hù)性大大提升. 我們可以采用類class, 結(jié)構(gòu)體struct, 枚舉enum來進(jìn)行分組存儲 key, 下面使用結(jié)構(gòu)體來示例.
// 賬戶信息
struct AccountInfo {
let userName = "userName"
let avatar = "avatar"
let password = "password"
let gender = "gender"
let age = "age"
}
// 登錄信息
struct LoginInfo {
let token = "token"
let userId = "userId"
}
// 配置信息
struct SettingInfo {
let font = "font"
let backgroundImage = "backgroundImage"
}
存取數(shù)據(jù):
let defaultStand = UserDefaults.standard
// 賬戶信息
defaultStand.set("Chilli Cheng", forKey: AccountInfo().avatar)
defaultStand.set(18, forKey: AccountInfo().age)
// 登錄信息
defaultStand.set("achj167", forKey: LoginInfo().token)
// 配置信息
defaultStand.set(24, forKey: SettingInfo().font)
let userName = defaultStand.string(forKey: AccountInfo().avatar)
let age = defaultStand.integer(forKey: AccountInfo().age)
let token = defaultStand.string(forKey: LoginInfo().token)
let font = defaultStand.integer(forKey: SettingInfo().font)
上下文
上面這種方式是不是比直接使用常量的效果更好? 但是仍然有個問題, 賬戶信息, 登錄信息, 配置信息都是屬于要存儲的信息, 那我們就可以把這三類信息歸到一個大類里, 在這個大類中有這三個小類, 三個小類作為大類的屬性, 既能解決一致性問題, 又能解決上下文的問題, 需要存儲到 UserDefaults 里面的數(shù)據(jù), 我只需要去特定的類中找到對應(yīng)分組里面的屬性即可. 示例:
struct UserDefaultKeys {
// 賬戶信息
struct AccountInfo {
let userName = "userName"
let avatar = "avatar"
let password = "password"
let gender = "gender"
let age = "age"
}
// 登錄信息
struct LoginInfo {
let token = "token"
let userId = "userId"
}
// 配置信息
struct SettingInfo {
let font = "font"
let backgroundImage = "backgroundImage"
}
}
存取數(shù)據(jù):
let defaultStand = UserDefaults.standard
// 賬戶信息
defaultStand.set("Chilli Cheng", forKey:UserDefaultKeys.AccountInfo().userName)
defaultStand.string(forKey: UserDefaultKeys.AccountInfo().userName)
上面的代碼看起來可讀性好了很多, 不僅是為了新接手的小伙伴能看懂, 更是為了我們自己過段時間能看懂. 我親眼見過自己寫的代碼看不懂反而要進(jìn)行重構(gòu)的小伙伴.
避免初始化
但是上面的代碼存在一個明顯的缺陷, 每次存取值的時候需要初始化 struct 出一個實例, 再訪問這個實例的屬性獲取 key 值, 其實是不必要的, 怎么才能做到不初始化實例就能訪問屬性呢? 可以使用靜態(tài)變量, 直接通過類型名字訪問屬性的值.
struct AccountInfo {
static let userName = "userName"
static let avatar = "avatar"
static let password = "password"
static let gender = "gender"
static let age = "age"
}
存取的時候:
defaultStand.set("Chilli Cheng", forKey: UserDefaultKeys.AccountInfo.userName)
defaultStand.string(forKey: UserDefaultKeys.AccountInfo.userName)
枚舉分組存儲
上面的方法雖然能基本滿足要求, 但是仍然不完美, 我們依然需要手動去設(shè)置 key, 當(dāng) key 值很多的時候, 需要一個個的設(shè)置, 那有沒有可以一勞永逸的辦法呢? 不需要我們自己設(shè)置 key 的值, 讓系統(tǒng)默認(rèn)給我們設(shè)置好 key 的初始值, 我們直接拿 key 去進(jìn)行存取數(shù)據(jù). Swift這么好的語言當(dāng)然可以實現(xiàn), 即用枚舉的方式, 枚舉不僅可以分組設(shè)置 key, 還能默認(rèn)設(shè)置 key 的原始值. 前提是我們需要遵守 String 協(xié)議, 不設(shè)置 rawValue 的時候, 系統(tǒng)會默認(rèn)給我們的枚舉 case 設(shè)置跟成員名字相同的原始值(rawValue), 我們就可以拿這個 rawValue 來作為存取數(shù)據(jù)的 key.
struct UserDefaultKeys {
// 賬戶信息
enum AccountInfo: String {
case userName
case age
}
}
// 存賬戶信息
defaultStand.set("Chilli Cheng", forKey: UserDefaultKeys.AccountInfo.userName.rawValue)
defaultStand.set(18, forKey: UserDefaultKeys.AccountInfo.age.rawValue)
// 取存賬戶信息
defaultStand.string(forKey: UserDefaultKeys.AccountInfo.userName.rawValue)
defaultStand.integer(forKey: UserDefaultKeys.AccountInfo.age.rawValue)
吼吼, 是不是感覺很方便, Swift 太棒了!
上面基本就能達(dá)到我們的目的, 既解決了一致性問題, 又有上下文知道我存取數(shù)據(jù)使用的 key 的含義. 但是代碼看起來很冗余, 我不就需要一個key 嘛, 干嘛非要鏈?zhǔn)秸{(diào)用那么多層呢? 還有就是為啥我非要寫 rawValue 呢? 如果新來的小伙伴不知道 rawValue 是什么鬼肯定懵逼.
優(yōu)化 key 值路徑
雖然上面的代碼能很好的達(dá)到目的, 但是寫法和使用上還是欠妥, 我們?nèi)孕枰^續(xù)改進(jìn), 上面的代碼主要存在兩個問題:
- key 值路徑太長
- rawValue 沒必要寫
我們先分析一下為什么會出現(xiàn)這個兩個問題:
key值的路徑長是因為我們想分組存儲 key, 讓key具有上下文, 可讀性更改,
rawValue 的作用是因為我們使用枚舉來存儲 key, 就不需要去手動設(shè)置 key 的初始值.
看起來簡直是"魚和熊掌不能兼得", 有什么辦法能解決"魚和熊掌"的問題呢?
那就是"砍掉抓著魚的熊掌". 也就是說我們必須先解決一個問題(先讓熊抓魚), 再想法"砍熊掌".
有了上面的一系列步驟, 解決第一個問題并不像剛開始一樣使用簡單的字符串, 而必須是使用枚舉, 在這個前提下去"抓魚". 也就是我能不能直接傳枚舉成員值進(jìn)去, 先利用枚舉的 rawValue 解決第一個問題,例如這樣使用:
defaultStand.set("Chilli Cheng", forKey: .userName)
defaultStand.string(forKey: .userName)
很明顯能夠?qū)崿F(xiàn), 只要給 userDefaults 擴(kuò)展自定義方法即可, 在自定義方法中調(diào)用系統(tǒng)的方法進(jìn)行存取, 為了使用方便我們擴(kuò)展類方法.示例:
extension UserDefaults {
enum AccountKeys: String {
case userName
case age
}
static func set(value: String, forKey key: AccountKeys) {
let key = key.rawValue
UserDefaults.standard.set(value, forKey: key)
}
static func string(forKey key: AccountKeys) -> String? {
let key = key.rawValue
return UserDefaults.standard.string(forKey: key)
}
}
// 存取數(shù)據(jù)
UserDefaults.set(value: "chilli cheng", forKey: .userName)
UserDefaults.string(forKey: .userName)
前置上下文
能實現(xiàn)上面的目的之一, 但是沒有上下文, 既然在 key 那里不能加, 換一個思路, 那就在前面加, 例如:
UserDefaults.AccountInfo.set(value: "chilli cheng", forKey: .userName)
UserDefaults.AccountInfo.string(forKey: .userName)
要實現(xiàn)上面的實現(xiàn)方式, 需要擴(kuò)展 UserDefaults, 添加 AccountInfo 屬性, 再調(diào)用 AccountInfo 的方法, key值由 AccountInfo 來提供, 因為AccountInfo 提供分組的 key, 由于是自定義的一個分組信息, 需要實現(xiàn)既定方法, 必然想到用協(xié)議呀, 畢竟 Swift 的協(xié)議很強(qiáng)大, Swift 就是面向協(xié)議編程的.
那我們先把自定義的方法抽取到協(xié)議中, 額, 但是協(xié)議不是只能提供方法聲明, 不提供方法實現(xiàn)嗎? 誰說的? 站出來我保證不打死他! Swift 中可以對協(xié)議 protocol 進(jìn)行擴(kuò)展, 提供協(xié)議方法的默認(rèn)實現(xiàn), 如果遵守協(xié)議的類/結(jié)構(gòu)體/枚舉實現(xiàn)了該方法, 就會覆蓋掉默認(rèn)的方法.
我們來試著實現(xiàn)一下, 先寫一個協(xié)議, 提供默認(rèn)的方法實現(xiàn):
protocol UserDefaultsSettable {
}
extension UserDefaultsSettable {
static func set(value: String, forKey key: AccountKeys) {
let key = key.rawValue
UserDefaults.standard.set(value, forKey: key)
}
static func string(forKey key: AccountKeys) -> String? {
let key = key.rawValue
return UserDefaults.standard.string(forKey: key)
}
}
只要我的 AccountInfo 類/結(jié)構(gòu)體/枚舉遵守這個協(xié)議, 就能調(diào)用存取方法了, 但是, 現(xiàn)在問題來了, 也是至關(guān)重要的問題, AccountKeys 從哪兒來? 我們上面是把 AccountKeys 寫在UserDefaults擴(kuò)展里面的, 在協(xié)議里面如何知道這個變量是什么類型呢? 而且還使用到了 rawValue, 為了通用性, 那就需要在協(xié)議里關(guān)聯(lián)類型, 而且傳入的值能拿到 rawValue, 那么這個關(guān)聯(lián)類型需要遵守 RawRepresentable 協(xié)議, 這個很關(guān)鍵!!!
protocol UserDefaultsSettable {
associatedtype defaultKeys: RawRepresentable
}
extension UserDefaultsSettable where defaultKeys.RawValue==String {
static func set(value: String?, forKey key: defaultKeys) {
let aKey = key.rawValue
UserDefaults.standard.set(value, forKey: aKey)
}
static func string(forKey key: defaultKeys) -> String? {
let aKey = key.rawValue
return UserDefaults.standard.string(forKey: aKey)
}
}
必須在擴(kuò)展中使用 where 子語句限制關(guān)聯(lián)類型是字符串類型, 因為 UserDefaults 的 key 就是字符串類型.
where defaultKeys.RawValue==String
在 UserDefaults 的擴(kuò)展中定義分組 key:
extension UserDefaults {
// 賬戶信息
struct AccountInfo: UserDefaultsSettable {
enum defaultKeys: String {
case userName
case age
}
}
// 登錄信息
struct LoginInfo: UserDefaultsSettable {
enum defaultKeys: String {
case token
case userId
}
}
}
存取數(shù)據(jù):
UserDefaults.AccountInfo.set(value: "chilli cheng", forKey: .userName)
UserDefaults.AccountInfo.string(forKey: .userName)
UserDefaults.LoginInfo.set(value: "ahdsjhad", forKey: .token)
UserDefaults.LoginInfo.string(forKey: .token)
打完收工, 既沒有手動去寫 key, 避免了寫錯的問題, 實現(xiàn)了key的一致性, 又實現(xiàn)了上下文, 能夠直接明白 key 的含義.
如果還有需要存儲的分類數(shù)據(jù), 同樣在 UserDefaults extension 中添加一個結(jié)構(gòu)體, 遵守 UserDefaultsSettable 協(xié)議, 實現(xiàn) defaultKeys 枚舉屬性, 在枚舉中設(shè)置該分類存儲數(shù)據(jù)所需要的 key.
注意: UserDefaultsSettable 協(xié)議中只實現(xiàn)了存取 string 類型的數(shù)據(jù), 可以自行在 UserDefaultsSettable 協(xié)議中添加 Int, Bool等類型方法. 雖然這種用法前期比較費勁, 但是不失為一種管理 UserDefaults 的比較好的方式.
如果大家有更好的方式, 歡迎交流.
歡迎大家斧正!