Swift:UserDefaults 協議( Swift 視角下的泛字符串類型 API )

做者:Andyy Hope,原文連接,原文日期:2016-11-01
譯者:Yake;校對:pmst;定稿:CMBgit

不管是從語言自己仍是項目代碼,Swift3 的革新無疑是一場「驚天海嘯」 ,一些讀者可能正奮戰在代碼遷移的前線。但即便有如此之多的改動, Swift 中依舊存在許多基於 Foundation 框架,泛字符串類型的 API 。這些 API 徹底沒有問題,只是...github

咱們對這種 API 有一種既愛又恨的感情:偏心它的靈活性;又恨一時粗心致使問題接踵而來。這簡直是在刀尖上編程。編程

Foundation 框架的開發者們之因此提供泛字符串類型的接口,是考慮到沒法準確預見咱們將來會如何使用這個框架。這些開發者們極盡本身的智慧、能力和知識,最終決定在某些 API 中使用字符串,這爲咱們開發人員帶來了無盡的可能性,也能夠說是一種黑魔法。swift

UserDefaults

今天的主題是我學習 iOS 開發初期最早熟悉的 API 之一。對於那些不熟悉它的人來講,它不過是對一系列信息的持久化存儲,例如一張圖片,一些應用的設置等。部分開發者偏向於認爲它是"輕量級的 Core Data 。儘管人們絞盡腦汁想要把它做爲替代品楔入,但結果代表它還遠遠不夠強大。框架

Stringly typed API

UserDefaults.standard.set(true, forKey: 「isUserLoggedIn」)
UserDefaults.standard.bool(forKey: "isUserLoggedIn")

這是 UserDefaults 在日常應用中的基礎用法,它向咱們提供了持久存儲和取值的簡單方法,在應用中隨處能夠覆蓋或者刪除數據。因爲缺乏一致性和上下文,咱們一不當心就會犯錯,但更有多是拼寫錯誤。在這篇文章當中,咱們將會改變 UserDefaults 在一般意義上的特性,並根據咱們的須要進行定製。性能

使用常量

let key = "isUserLoggedIn"
UserDefaults.standard.set(true, forKey: key)
UserDefaults.standard.bool(forKey: key)

若是你聽從這種奇妙的技巧,我保證你很快就能將代碼寫得更好。若是你須要屢次重複使用一個字符串,那麼將它轉換成一個常量,並在你的餘生一直遵照這種規則,而後記得下輩子謝謝我。學習

分組常量

struct Constants {
    let isUserLoggedIn = "isUserLoggedIn"
}
...
UserDefaults.standard
   .set(true, forKey: Constants().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants().isUserLoggedIn)

一種能夠幫咱們維持一致性的模式就是將咱們全部重要的默認常量分組寫在同一個地方。這裏咱們建立了一個常量結構體來存儲並指向咱們的默認值。this

還有一個建議是將你的屬性名字設置成它對應的值,尤爲是跟默認值打交道的時候。這樣作能夠簡化你的代碼並使屬性在總體上有更好的一致性。拷貝屬性名,將他們粘貼在字符串中,這樣能夠避免拼寫錯誤。spa

let isUserLoggedIn = "isUserLoggedIn"

添加上下文

struct Constants {
    struct Account
        let isUserLoggedIn = "isUserLoggedIn"
    }
}
...
UserDefaults.standard
   .set(true, forKey: Constants.Account().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account().isUserLoggedIn)

建立一個常量結構體徹底沒有問題,可是在咱們寫代碼的時候記得提供上下文。咱們努力的目標是讓本身的代碼對任何人都具備較高的可讀性,包括咱們本身。.net

Constants().token // Huh?

token 是什麼意思?當有人試圖搞清楚這個 token 的意義是什麼的時候,缺乏命名空間上下文使得新人或者不熟悉代碼的人很難搞清楚這意味着什麼,甚至包括一年後的原做者。

Constants.Authentication().token // better

避免初始化

struct Constants {
    struct Account
        let isUserLoggedIn = "isUserLoggedIn"
    }
    private init() { }
}

咱們絕對不打算,也不想讓常量結構體被初始化,因此咱們把初始化方法聲明爲私有方法。這只是一個預防性措施,但我仍然推薦這麼作。至少這樣作能夠避免咱們在只想要靜態變量時卻不當心聲明瞭實例變量。說到靜態變量...

靜態變量

struct Constants {
    struct Account
        static let isUserLoggedIn = "isUserLoggedIn"
    }
    ...
}
...
UserDefaults.standard
   .set(true, forKey: Constants.Account.isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account.isUserLoggedIn)

你可能已經注意到了,咱們每次獲取 key ,都須要初始化它所屬的結構體。與其每次都這麼作,咱們不如把它聲明爲靜態變量。

咱們使用 static 而非 class 關鍵字,是由於結構體做爲存儲類型時只容許使用前者。依據 Swift 的編譯規則,結構體不能使用 class 聲明屬性。但若是你在一個類中使用 static 聲明屬性,這跟使用 final class 聲明屬性是同樣的。

final class name: String
static name: String
// final class == static

使用枚舉類型避免拼寫錯誤

enum Constants {
    enum Account : String {
        case isUserLoggedIn
    }
    ...
}
...
UserDefaults.standard
    .set(true, forKey: Constants.Keys.isUserLoggedIn.rawValue)
UserDefaults.standard
    .bool(forKey: Constants.Keys.isUserLoggedIn.rawValue)

文章中咱們提到了,爲了一致性咱們須要使屬性能反映出他們的值。這裏咱們會將這種一致性更進一步,採用 enum case 來代替 static let 來將這個過程自動化。

你可能已經注意到了,咱們已經建立了 Account 並讓其遵照 String 協議,而 Stirng 遵照了 RawRepresentable 協議。這麼作是由於,若是咱們不給每一個 case 提供一個 RawValue ,這個值將和聲明的 case 保持一致。這麼作會減小不少手動的輸入或者複製粘貼字符串,減小錯誤的發生。

// Constants.Account.isUserLoggedIn.rawValue == "isUserLoggedIn"

到目前爲止咱們已經使用 UserDefaults 作了一些很酷的事情,但其實咱們作的還不夠。最大的問題是咱們仍然在使用泛字符串類型 API ,即便咱們已經對字符串作了一些修飾,但對於項目來講還不夠好。

在咱們的認知中,語言提供給咱們什麼,咱們就只能幹什麼。然而 Swift 是一門如此棒的語言,咱們已經在挑戰過去寫 Objective-C 時學習到和了解的知識。接下來,讓咱們回到廚房給這些 API 加些語法糖做料。

API 目標

UserDefaults.standard.set(true, forKey: .isUserLoggedIn) 
// #APIGoals

下面,咱們會力爭建立一些在與 UserDefaults 打交道時更好用的 API ,以此知足咱們的須要。而比較好的作法莫過於使用協議擴展。

BoolUserDefaultable

protocol BoolUserDefaultable {
    associatedType BoolDefaultKey : RawRepresentable
}

首先咱們來爲布爾類型的 UserDefalts 建立一個協議,這個協議很簡單,沒有任何變量和須要實現的方法。然而,咱們提供了一個叫作 BoolDefaultKey 的關聯類型,這個類型遵照 RawRepresentable 協議,接下來你會明白爲何這麼作。

擴展

extension BoolUserDefaultable 
    where BoolDefaultKey.RawValue == String { ... }

若是咱們準備遵照協議的 Crusty 定律,首先聲明一個協議擴展。而且使用一個 where 句法,限制擴展只適用於關聯類型的 RawValue 是字符串的狀況。

每個協議,都有一個至關且相符合的協議擴展- Crusty 第三定律。

UserDefaultSetter 方法

// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = key.rawValue
    UserDefaults.standard.set(value, forKey: key)
}
static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = key.rawValue
    return UserDefaults.standard.bool(forKey: key)
}

是的,這是對標準 UserDefaultsAPI 的簡單封裝。咱們這麼作是由於這樣代碼的可讀性會更高,由於你只需傳入簡單的枚舉值而不須要傳入冗長的字符串(校對者注:摒棄相似下面 Aint.Nobody.Got.Time.For.this.rawValue 這種路徑式字符串)。

UserDefaults.set(false, 
    forKey: Aint.Nobody.Got.Time.For.this.rawValue)

一致性

extension UserDefaults : BoolUserDefaultSettable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn
    }
}

是的,咱們準備擴展 UserDefaults ,讓它遵照 BoolDefaultSettable 並提供一個名叫 BoolDefaultKey 的關聯類型,這個關聯類型遵照協議 RawRepresentable

// Setter
UserDefaults.set(true, forKey: .isUserLoggedIn)
// Getter
UserDefaults.bool(forKey: .isUserLoggedIn)

咱們再一次挑戰了只能使用已有 API 的規範,而定義了咱們本身的 API 。這是由於,當咱們擴展了 UserDefaults ,使用咱們本身的 API 卻丟失了上下文。若是這個 key 不是 .isUserLoggedIn ,咱們還會理解它到底和什麼關聯麼?

UserDefaults.set(true, forKey: .isAccepted) 
// Huh? isAccepted for what?

這個 key 的含義很模糊,它可能表明任何東西。即便看起來沒什麼,但提供上下文老是有好處的。

「有可是不須要」,比「不須要也沒有」要好。

不用擔憂,添加上下文很簡單。咱們只須要給這個 key 添加一個命名空間。在這個例子中,咱們建立了一個 Account 的命名空間,它包含了 isUserLoggedIn 這個 key

struct Account : BoolUserDefaultSettable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn
    }
    ...
}
...
Account.set(true, forKey: .isUserLoggedIn)

衝突

ley account = Account.BoolDefaultKey.isUserLoggedIn.rawValue
let default = UserDefaults.BoolDefaultKey.isUserLoggedIn.rawValue
// account == default
// "isUserLoggedIn" == "isUserLoggedIn"

擁有兩種分別遵照同一協議並提供了相同的 key 的類型絕對是有可能的,做爲編程人員,若是咱們不能在項目落地以前解決這個問題,那咱們絕對要熬夜了。絕對不能冒着拿某個 key 改變另一個 key 的值的風險。因此咱們應該爲咱們本身的 key 建立命名空間。

命名空間

protocol KeyNamespaceable { }

咱們確定要爲此建立一個協議了,誰叫我們是 Swift 開發人員。協議一般是解決任何當前面臨問題的首要嘗試。若是協議是巧克力醬,咱們就在全部的食物上面都抹上它,即便是牛排。知道咱們有多愛協議了嗎?

extension KeyNamespaceable { 
  func namespace<T>(_ key: T) -> String where T: RawRepresentable {
        return "\(Self.self).\(key.rawValue)"
  }
}

這是一個簡單的方法,它將傳入的字符串作了合併,並用"."來將這兩個對象分開,一個是類的名字,一個是 keyRawValue 。咱們也利用泛型來容許咱們的方法接收一個遵照 RawRepresentable 協議的泛型參數 key

protocol BoolUserDefaultSettable : KeyNamespaceable

建立了命名空間協議以後,咱們再來看以前的 BoolUserDefaultSettable 協議並讓他遵照 KeyNamespaceable 協議,修改以前的擴展來讓他發揮新功能的優點。

// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = namespace(key)
    UserDefaults.standard.set(value, forKey: key)
}
static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = namespace(key)
    return UserDefaults.standard.bool(forKey: key)
}
...
ley account = namespace(Account.BoolDefaultKey.isUserLoggedIn)
let default = namespace(UserDefaults.BoolDefaultKey.isUserLoggedIn)
// account != default
// "Account.isUserLoggedIn" != "UserDefaults.isUserLoggedIn"

上下文

因爲建立了這個協議,咱們可能會感受從 UserDefaultsAPI 中解放了,也許會所以陶醉在協議的魅力之中。在這個過程當中,咱們經過將 key 移入有意義的命名空間來建立上下文。

Account.set(true, forKey: .isUserLoggedIn)

但因爲這個 API 沒有完整的意義咱們仍是必定程度上丟失了上下文。一眼看上去,代碼中沒有任何信息告訴咱們這個布爾值會被持久存儲。爲了讓一切圓滿,咱們準備擴展 UserDefaults 並把咱們的默認類型放進去。

extension UserDefaults {
    struct Account : BoolUserDefaultSettable { ... }
}
...
UserDefaults.Account.set(true, forKey: .isUserLoggedIn)
UserDefaults.Account.bool(forKey: .isUserLoggedIn)

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索