不管是從語言自己仍是項目代碼,Swift3
的革新無疑是一場「驚天海嘯」 ,一些讀者可能正奮戰在代碼遷移的前線。但即便有如此之多的改動, Swift
中依舊存在許多基於 Foundation
框架,泛字符串類型的 API
。這些 API
徹底沒有問題,只是...github
咱們對這種 API
有一種既愛又恨的感情:偏心它的靈活性;又恨一時粗心致使問題接踵而來。這簡直是在刀尖上編程。編程
Foundation
框架的開發者們之因此提供泛字符串類型的接口,是考慮到沒法準確預見咱們將來會如何使用這個框架。這些開發者們極盡本身的智慧、能力和知識,最終決定在某些 API
中使用字符串,這爲咱們開發人員帶來了無盡的可能性,也能夠說是一種黑魔法。swift
今天的主題是我學習 iOS 開發初期最早熟悉的 API
之一。對於那些不熟悉它的人來講,它不過是對一系列信息的持久化存儲,例如一張圖片,一些應用的設置等。部分開發者偏向於認爲它是"輕量級的 Core Data
。儘管人們絞盡腦汁想要把它做爲替代品楔入,但結果代表它還遠遠不夠強大。框架
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
第三定律。
UserDefault
的 Setter
方法// 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) }
是的,這是對標準 UserDefaults
的 API
的簡單封裝。咱們這麼作是由於這樣代碼的可讀性會更高,由於你只需傳入簡單的枚舉值而不須要傳入冗長的字符串(校對者注:摒棄相似下面 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)" } }
這是一個簡單的方法,它將傳入的字符串作了合併,並用"."來將這兩個對象分開,一個是類的名字,一個是 key
的 RawValue
。咱們也利用泛型來容許咱們的方法接收一個遵照 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"
因爲建立了這個協議,咱們可能會感受從 UserDefaults
的 API
中解放了,也許會所以陶醉在協議的魅力之中。在這個過程當中,咱們經過將 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。