Swift中安全優雅的使用UserDefaults

蹺腳啖牛肉發表於2018-05-17

原文在這裡
納尼? 如此簡單的 UserDefaults 怎麼去優雅的使用? 這麼簡單的還能玩出花來? 沒毛病吧?

嗯, 沒毛病!


Objective-C 中的 NSUserDefaults 我們並不陌生, 通常作為資料持久化的一種方式, 一般用來儲存使用者資訊和基礎配置資訊. Swift 中使用 UserDefaults 來替代 NSUserDefaults, 兩者的使用基本相同.

let defaults = UserDefaults.standard
defaults.set(123, forKey: "defaultKey")
defaults.integer(forKey: "defaultKey")
複製程式碼

Objective-C中需要呼叫 synchronize 方法進行同步, 但是在Swift中已經廢棄了該方法, 所以不需要手動去呼叫.
-synchronize is deprecated and will be marked with the NS_DEPRECATED macro in a future release.

上面的用法是最基本的用法, 也是我們平常開發中使用頻率最高的用法, 但也是最危險的用法, 為什麼呢?

  1. 在應用內部我們可以隨意地覆蓋和刪除儲存的值, 直接使用字串來作為儲存資料的 key 是非常危險的, 容易導致存資料時使用的 key 和取資料的時候使用的 key 不一致.
  2. UserDefaults.standard 是一個全域性的單例, 如果需要儲存賬戶資訊(AccountInfo), 配置資訊(SettingInfo), 此時按照最基本的使用方式, 簡單的使用 key 來存取資料, 那麼 key 值會隨著儲存的資料越來越多, 到時候不管是新接手的小夥伴還是我們自己都很難明白每個 key 值對應的意義. 也就是說我們不能根據方法呼叫的上下文明確知道我存取資料的具體含義, 程式碼的可讀性和可維護性就不高.所以我們要利用 Swift 強大的靈活性來讓我們使用 UserDefaults 存取資料的時候更加便捷和安全.

所以要想把 UserDefaults 玩出花來就得解決下面兩個問題:

  • 一致性
  • 上下文

一致性

使用 UserDefaults 存取資料時使用的 key 值不同就會導致存在一致性問題. 原因就在於通常我們在存取資料的時候, 手動鍵入 key 或者複製貼上 key 可能會出錯, 輸入的時候也很麻煩. 那我們的目的就比較明確了, 就是為了讓存取的 key 一致, 即使改了其中一個另外一個也隨之更改.

解決辦法:

  • 常量儲存
  • 分組儲存

常量儲存字串

既然涉及到兩個重複使用的字串, 很容易就想到用常量儲存字串, 只有在初始化的時候設定 key 值, 存取的時候拿來用即可, 簡單粗暴的方式.

let defaultStand = UserDefaults.standard
let defaultKey = "defaultKey"
defaultStand.set(123, forKey: defaultKey)
defaultStand.integer(forKey: defaultKey)
複製程式碼

是不是感覺有點換湯不換藥? 上面使用常量儲存 key 值, 雖然能夠保證存取的時候 key 值相同, 但是在設定 key 值的時候稍顯麻煩.
最重要的一點就是如果需要存很多賬戶資訊或者配置資訊的時候, 按照這種方式都寫在同一處地方就稍微欠妥, 比如下面這個場景, 在 app 啟動後, 需要儲存使用者資訊和登入資訊, 使用者資訊裡面包含: userName, avatar, password, gender等, 登入資訊裡包含: token, userId, timeStamp等等, 也就說需要存兩類不同的資訊, 那麼此時這種方式就不合時宜了, 我們就會想辦法把同類的資訊歸為一組, 進行分組存取.

分組儲存

分組儲存 key 可以把儲存資料按不同類別區分開, 程式碼的可讀性和可維護性大大提升. 我們可以採用類class, 結構體struct, 列舉enum來進行分組儲存 key, 下面使用結構體來示例.

// 賬戶資訊
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"
}
複製程式碼

存取資料:

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 裡面的資料, 我只需要去特定的類中找到對應分組裡面的屬性即可. 示例:

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"
    }
}
複製程式碼

存取資料:

let defaultStand = UserDefaults.standard
// 賬戶資訊
defaultStand.set("Chilli Cheng", forKey:UserDefaultKeys.AccountInfo().userName)
defaultStand.string(forKey: UserDefaultKeys.AccountInfo().userName)
複製程式碼

上面的程式碼看起來可讀性好了很多, 不僅是為了新接手的小夥伴能看懂, 更是為了我們自己過段時間能看懂. 我親眼見過自己寫的程式碼看不懂反而要進行重構的小夥伴.

避免初始化

但是上面的程式碼存在一個明顯的缺陷, 每次存取值的時候需要初始化 struct 出一個例項, 再訪問這個例項的屬性獲取 key 值, 其實是不必要的, 怎麼才能做到不初始化例項就能訪問屬性呢? 可以使用靜態變數, 直接通過型別名字訪問屬性的值.

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)
複製程式碼

列舉分組儲存

上面的方法雖然能基本滿足要求, 但是仍然不完美, 我們依然需要手動去設定 key, 當 key 值很多的時候, 需要一個個的設定, 那有沒有可以一勞永逸的辦法呢? 不需要我們自己設定 key 的值, 讓系統預設給我們設定好 key 的初始值, 我們直接拿 key 去進行存取資料. Swift這麼好的語言當然可以實現, 即用列舉的方式, 列舉不僅可以分組設定 key, 還能預設設定 key 的原始值. 前提是我們需要遵守 String 協議, 不設定 rawValue 的時候, 系統會預設給我們的列舉 case 設定跟成員名字相同的原始值(rawValue), 我們就可以拿這個 rawValue 來作為存取資料的 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 太棒了! 上面基本就能達到我們的目的, 既解決了一致性問題, 又有上下文知道我存取資料使用的 key 的含義. 但是程式碼看起來很冗餘, 我不就需要一個key 嘛, 幹嘛非要鏈式呼叫那麼多層呢? 還有就是為啥我非要寫 rawValue 呢? 如果新來的小夥伴不知道 rawValue 是什麼鬼肯定懵逼.

優化 key 值路徑

雖然上面的程式碼能很好的達到目的, 但是寫法和使用上還是欠妥, 我們仍需要繼續改進, 上面的程式碼主要存在兩個問題:

  • key 值路徑太長
  • rawValue 沒必要寫

我們先分析一下為什麼會出現這個兩個問題: key值的路徑長是因為我們想分組儲存 key, 讓key具有上下文, 可讀性更改, rawValue 的作用是因為我們使用列舉來儲存 key, 就不需要去手動設定 key 的初始值.

看起來簡直是"魚和熊掌不能兼得", 有什麼辦法能解決"魚和熊掌"的問題呢?
那就是"砍掉抓著魚的熊掌". 也就是說我們必須先解決一個問題(先讓熊抓魚), 再想法"砍熊掌".

有了上面的一系列步驟, 解決第一個問題並不像剛開始一樣使用簡單的字串, 而必須是使用列舉, 在這個前提下去"抓魚". 也就是我能不能直接傳列舉成員值進去, 先利用列舉的 rawValue 解決第一個問題,例如這樣使用:

defaultStand.set("Chilli Cheng", forKey: .userName)
defaultStand.string(forKey: .userName)
複製程式碼

很明顯能夠實現, 只要給 userDefaults 擴充套件自定義方法即可, 在自定義方法中呼叫系統的方法進行存取, 為了使用方便我們擴充套件類方法.示例:

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)
    }
}

// 存取資料
UserDefaults.set(value: "chilli cheng", forKey: .userName)
UserDefaults.string(forKey: .userName)
複製程式碼

前置上下文

能實現上面的目的之一, 但是沒有上下文, 既然在 key 那裡不能加, 換一個思路, 那就在前面加, 例如:

UserDefaults.AccountInfo.set(value: "chilli cheng", forKey: .userName)
UserDefaults.AccountInfo.string(forKey: .userName)
複製程式碼

要實現上面的實現方式, 需要擴充套件 UserDefaults, 新增 AccountInfo 屬性, 再呼叫 AccountInfo 的方法, key值由 AccountInfo 來提供, 因為AccountInfo 提供分組的 key, 由於是自定義的一個分組資訊, 需要實現既定方法, 必然想到用協議呀, 畢竟 Swift 的協議很強大, Swift 就是面向協議程式設計的.
那我們先把自定義的方法抽取到協議中, 額, 但是協議不是隻能提供方法宣告, 不提供方法實現嗎? 誰說的? 站出來我保證不打死他! Swift 中可以對協議 protocol 進行擴充套件, 提供協議方法的預設實現, 如果遵守協議的類/結構體/列舉實現了該方法, 就會覆蓋掉預設的方法. 我們來試著實現一下, 先寫一個協議, 提供預設的方法實現:

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 類/結構體/列舉遵守這個協議, 就能呼叫存取方法了, 但是, 現在問題來了, 也是至關重要的問題, AccountKeys 從哪兒來? 我們上面是把 AccountKeys 寫在UserDefaults擴充套件裡面的, 在協議裡面如何知道這個變數是什麼型別呢? 而且還使用到了 rawValue, 為了通用性, 那就需要在協議裡關聯型別, 而且傳入的值能拿到 rawValue, 那麼這個關聯型別需要遵守 RawRepresentable 協議, 這個很關鍵!!!

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)
    }
}
複製程式碼

必須在擴充套件中使用 where 子語句限制關聯型別是字串型別, 因為 UserDefaults 的 key 就是字串型別. where defaultKeys.RawValue==String

在 UserDefaults 的擴充套件中定義分組 key:

extension UserDefaults {
    // 賬戶資訊
    struct AccountInfo: UserDefaultsSettable {
        enum defaultKeys: String {
            case userName
            case age
        }
    }
    
    // 登入資訊
    struct LoginInfo: UserDefaultsSettable {
        enum defaultKeys: String {
            case token
            case userId
        }
    }
}
複製程式碼

存取資料:

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, 避免了寫錯的問題, 實現了key的一致性, 又實現了上下文, 能夠直接明白 key 的含義. 如果還有需要儲存的分類資料, 同樣在 UserDefaults extension 中新增一個結構體, 遵守 UserDefaultsSettable 協議, 實現 defaultKeys 列舉屬性, 在列舉中設定該分類儲存資料所需要的 key.

注意: UserDefaultsSettable 協議中只實現了存取 string 型別的資料, 可以自行在 UserDefaultsSettable 協議中新增 Int, Bool等型別方法. 雖然這種用法前期比較費勁, 但是不失為一種管理 UserDefaults 的比較好的方式.
如果大家有更好的方式, 歡迎交流.

歡迎大家斧正!

相關文章