一年前,在 Swift 推出不久後,我觀察到許多 iOS 開發者仍然以 Objective-C 的開發習慣來寫 Swift。而在我眼中,Swift 是一門全新的語言,有別於 Objective-C 的語法、設計哲學乃至發展潛力,所以咱們更應探索出一條屬於 Swift 獨有風格的發展道路。我在以前的文章 Swifty methods 中已經探討過在 Swift 中如何清晰、明確地對方法進行命名,隨後我開始連載 Swifty API 系列文章,同時將這一想法付諸實踐,探索如何設計更加簡單易用的接口 API。git
在該系列(Swifty API)第一篇文章中,咱們對 NSUserDefaults
API 進行了改造:github
NSUserDefaults.standardUserDefaults().stringForKey("color")
NSUserDefaults.standardUserDefaults().setObject("red", forKey: "color")
|
改造以後看上去像這樣:swift
Defaults["color"].string
Defaults["color"] = "red"
|
相較以前的 get 和 set 方法,改造後的結果更加簡單明瞭,同時也修正了一致性問題,使其更符合 Swift 的使用風格。這看上去是至關大的改進。數組
可是,隨着我對 Swift 深刻學習,以及真正在項目中使用這些由我親手締造的 API 後,我才意識到這些 API 離真正的原生 Swift 風格還有至關大的差距。在以前的 API 設計中,我從 Ruby 和 Swift 的語法中汲取靈感來構建本身的 API,這一點雖然值得確定,可是咱們並無將其真正提高到語義學的高度。僅僅是在外表裹了層 Swift 風格的外衣,而內部機制仍然是以 Objective-C 的形式在運做。安全
「API 不是那麼 Swift 化」,聽上去並非一個讓咱們從頭開始的好理由,雖然類似的 API 更容易學習,但咱們不想這麼教條。咱們不只僅想要設計出來的 API 看上去更加 Swift 化,還但願能在 Swift 的運行機制下更好地工做。這樣看來,咱們以前設計的 NSUserDefaults
存在一些小問題:app
假設你有一個關於用戶喜愛顏色的設置選項:框架
Defaults["color"] = "red"
// App 中的其餘一處:
Defaults["colour"].string // => nil
|
啊哦,一旦不當心把鍵名(key name)寫錯了,就會出現 Bug :(學習
同理咱們放一個 date
對象到 defaults
中:測試
Defaults["deadline"] = NSDate.distantFuture()
Defaults["deadline"].data // => nil
|
這一次在 getter
方法中把 date
拼錯成了 data
,結果是 nil
,又得到 bug 一枚。你或許認爲這種狀況並不會常常發生,但爲何每次咱們要對取回(getter
)的對象指定類型呢?這確實有點煩人。編碼
再來一個例子(這個例子咱們在賦值的時候就錯了,取回的結果固然是 nil
):
Defaults["deadline"] = NSData()
Defaults["deadline"].date // => nil
|
顯然咱們想要「如今的時間」的日期而不是一個「空的data值」!
最後觀察下面的代碼:
Defaults["magic"] = 3.14
Defaults["magic"] += 10
Defaults["magic"] // => 13
|
在第一篇文章中,咱們從新定義了 +=
,使其可以在個人新 API 下正常工做,但這兒有個缺陷:只能從傳遞進來的參數進行類型推斷(Int
or Double
)。也就是說若是你參數傳一個整數(Int
)10,運算後的結果是 13.14(Double
)類型,但最終返回的結果仍是會以上一次傳入的參數類型爲基準,決定最終的返回值。這個例子最後返回值爲 13,砍掉了小數部分,顯然是個 bug。
你又或許認爲以上都是純理論問題,在真實世界裏並不會發生。先彆着急下結論,仔細想一想,這些拼錯變量名、方法名和傳遞一個錯誤類型的參數其實均可以歸爲同一類型的 bug,而這些 bug 在平常開發中是常有之事。若是你正在使用一門須要編譯的靜態類型語言,那麼你更應該依賴編譯器給你的反饋而不是過後去測試,更重要的是,花費精力在編譯期進行檢查也會在未來給你帶來豐厚的紅利,這不只僅是在首次寫代碼時才能享受這種編譯器檢查帶來的好處,在以後的重構中,你也能減小不少沒必要要的 bug。這裏提供一些小建議,可讓將來的你免受 bug 之苦。
致使這些問題的根源在於:沒有定義關於 user defaults 的靜態結構。
在早先的設計中,我意識到這個問題,因而將各類類型封裝在了 NSUserDefaults
內部的 Proxy
對象中。調用時你能夠經過下標(subscript)得到一個 Proxy
對象,而後再經過 Proxy 提供的訪問方法來實現特定類型的訪問。
Defaults["color"].string
Defaults["launchCount"].int
|
採用上面這種方式,比你本身實現 getter
方法或手動對 AnyObject
類型轉換要好不少。
但這是一個 hack,並不算真正的解決方案。爲了對 API 實現真正意義上的改進,咱們須要收集有關 user default keys 的信息,以後提供給編譯器。
如今回想下那位長者傳授給咱們的人生經驗。一般不變的常量字符串,爲了不拼寫錯誤,會在一開始就以 string keys
的形式定義,隨後使用時編譯器也會自動補全:
let colorKey = "color"
|
讓咱們帶上類型信息:
class DefaultsKey<ValueType> {
let key: String
init(_ key: String) {
self.key = key
}
}
let colorKey = DefaultsKey<String?>("color")
|
咱們將 key name 封裝在一個對象中,而且將值類型植入到泛型參數中。如今咱們能夠定義一個新的 NSUserDefaults
下標,用來接收這些 keys:
extension NSUserDefaults {
subscript(key: DefaultsKey<String?>) -> String? {
get { return stringForKey(key.key) }
set { setObject(newValue, forKey: key.key) }
}
}
|
這裏是結果:
let key = DefaultsKey<String?>("color")
Defaults[key] = "green"
Defaults[key] // => "green", typed as String?
|
沒錯,就這麼簡單,語法和功能稍後再來完善。咱們經過這個小技巧,修復了許多問題。好比沒辦法再輕易拼錯 key name 了,由於他只能定義一次。也不能隨便就賦值一個不匹配的類型了,由於你這麼作編譯器會報錯。最後也沒必要寫 .string
,由於編譯器已經知道咱們想要的類型了。
此外,咱們或許應該使用泛型來定義 NSUserDefaults
的下標(subscripts
),而不是手動輸入全部須要的類型。不過想法老是美好的,現實倒是殘酷的,Swift 的編譯器目前還不支持泛型下標。(╯‵□′)╯︵┻━┻ 方括號可能看上去還不錯,別再糾結語法了,咱們讓 setting 和 getting 方法更加泛型化就行了。
等等,你尚未見識過下標 subscripts
的能耐!
考慮下面這種寫法:
var array = [1, 2, 3]
array.first! +=
10
|
徹底不能經過編譯!咱們嘗試對數組內部的整數進行加法操做,但這對於 Swift 來講是作不到的。整數具備值語義,是不可變的。當他們從某些地方返回時,你不能直接去修改他們的值,這是由於他們並不存在於表達式以外,僅算是瞬時狀態下的一份拷貝罷了。
改換變量來作就沒問題:
var number = 1
number +=
10
|
注意,實際並無真正意義上改變 1 這個整數,而只是修改了變量,爲其分配了一個新值而已。
再來看看下面這段代碼:
var array = [1, 2, 3]
array[
0] += 10
array
// => [11, 2, 3]
|
結果終於如你所願了,不是嗎?這和你想象中的同樣,但是爲何這麼作就能夠了呢?
觀察一下,在 Swift 中,下標和裏面的值類型彷佛也合做地很是愉快。咱們能夠經過下標來修改數組裏的值,是由於他在內部實現了 getter
和 setter
方法。編譯器層面所作的工做是將 array[0] += 10
重寫爲 array[0] = array[0] + 10
。若是你只實現了 getter
下標 subscript
,而沒有實現 setter
,是不會正常工做的。
這不只僅是數組(Array)特有的黑魔法,這是下標(subscript)語義精心設計後的結果,咱們能夠在本身實現的 subscripts
免費得到這種特性,好比咱們還能夠這麼玩:
Defaults[launchCountKey]++
Defaults[volumeKey] -= 0.1
Defaults[favoriteColorsKey].append("green")
Defaults[stringKey] += "… can easily be extended!"
|
有意思吧,要知道在 API 1.0 版本,咱們僅僅模仿字典那樣使用下標,並無利用上面介紹的這種語義。
咱們還添加了一些 +=
、++
這樣的操做符,可是這種行爲比較危險,主要依賴於編譯器的魔法實現。在這裏咱們經過將類型信息封裝在 key 中,而後定義了 subscript
的 getter
和 setter
方法,如今整個世界看上去運轉正常。
在老版本 API 設計中,使用字符串 key 的好處在於你能夠按需使用,而不用去建立任何中間對象。
而在目前改進的新版本中,每次使用前都要建立鍵對象(key object
)好像沒什麼道理,何況這會帶來可怕的重複以及抵消掉靜態類型帶來的好處。因此讓咱們再想一想如何可以更好地組織 defaults keys
:
一種解決方案就是在類層級(class level)定義這些 keys:
class Foo {
struct Keys {
static let color = DefaultsKey<String>("color")
static let counter = DefaultsKey<Int>("counter")
}
func fooify() {
let color = Defaults[Keys.color]
let counter = Defaults[Keys.counter]
}
}
|
這彷佛已是 Swift 關於字符型 keys 的標準實踐了。
另外一種解決方案是利用 Swift 的隱式成員表達式,此功能的最多見用途是枚舉。當一個方法須要一個枚舉類型 Direction
做爲參數,你能夠傳遞 .Right
。編譯器可以推斷出真正的參數類型Direction.Right
。這裏有個冷知識:這種特性(隱式成員表達式)一樣適用於方法參數是靜態成員類型的情形,例如你能夠在一個須要 CGRect
類型作參數的方法中,使用 .zeroRect
來代替 CGRect.zeroRect
。
事實上,咱們能夠經過把鍵定義爲 DefaultsKey
上的靜態常量來作相同的事情。好啦,差很少了,最後爲了消除編譯器上的限制,咱們須要一個稍微不一樣的定義:
class DefaultsKeys {}
class DefaultsKey<ValueType>: DefaultsKeys { ... }
extension DefaultsKeys {
static let color = DefaultsKey<String>("color")
}
|
試一下效果,哇,不錯哦!
Defaults[.color] = "red"
|
是否是很炫酷?站在調用者的角度,如今比以前用傳統字符串的方式顯得再也不那麼冗餘,開發者的代碼量減小了,讀起來也更直觀。有沒有感到很興奮,若是我告訴你這一切都是免費得到的,你會不會更開心。
(這項技術的一個缺陷就是沒有命名空間機制,在大工程中仍是老實採用鍵結構體 Keys struct
的方式更好一些。)
在前一版設計的 API 中,咱們讓全部的 getters 都返回可選值,不過我不大喜歡 NSUserDefaults
處理不一樣類型時缺少一致性,對於字符串,缺失值將返回 nil
,可是對於數字和布爾值,你將會獲得 0
和 false
。
我很快意識到這種方式缺點是太冗長。大多數時候咱們並不關心 nil
的情形,只但願在這種狀況下獲得一個默認值,僅此而已。而每次咱們經過下標(subscripts)得到一個可選值後,都要先解封包作判斷,再決定返回解包值仍是預設的默認值。
Oleg Kokhtenko 針對這個問題提出了解決方案,除了標準的可選返回值的 getter
方法,咱們還添加了一組 getter
方法,這些方法都以標誌性的 -Value
結尾,而且結果爲 nil
時會返回默認值代替,這樣類型更加明確:
Defaults["color"].stringValue // 默認獲得""
Defaults["launchCount"].intValue // 默認獲得0
Defaults["loggingEnabled"].boolValue // 默認獲得false
Defaults["lastPaths"].arrayValue // 默認獲得[]
Defaults["credentials"].dictionaryValue // 默認獲得[:]
Defaults["hotkey"].dataValue // 默認獲得NSData()
|
咱們能夠在靜態類型體制下作一樣的事情,下面爲 optional 和非 optional 類型各提供一個 subscript
變體。
extension NSUserDefaults {
subscript(key: DefaultsKey<NSData?>) -> NSData? {
get { return dataForKey(key.key) }
set { setObject(newValue, forKey: key.key) }
}
subscript(key: DefaultsKey<NSData>) -> NSData {
get { return dataForKey(key.key) ?? NSData() }
set { setObject(newValue, forKey: key.key) }
}
}
|
我喜歡這麼作,由於這樣就不用依賴協定約定(type
和 typeValue
),將空值轉換爲各類類型的默認值。而是使用已經在 user defaults key 中定義好的類型,剩下的工做就交給編譯器吧。
我經過添加這些類型的下標來擴大支持的類型範圍:String
,Int
,Double
,Bool
,NSData
,[AnyObject]
,[String: AnyObject]
,NSString
,NSArray
,NSDictionary
(還包含他們的可選變體,注意 NSDate?
,NSURL?
,AnyObject?
沒有對應的非可選部分,由於這些類型的默認值沒有意義)。
還要注意一點,字符串(strings)、字典(dictionaries)和數組(arrays)同時存在於 Swift 基本庫和 Cocoa Foundation 框架中。而咱們優先考慮 Swift 原生類型,但這些類型並不具有他們在 Cocoa 框架中的一些能力,不過若是真正須要,我會讓事情簡單一些。
提到數組,爲何把咱們只限制沒有類型化的數組?由於在大多數狀況下,user defaults
中存儲的數組裏面的元素都是同一類型的,好比 String
,Int
,NSData
。
由於不能定義泛型下標,咱們來建立一對泛型 helper
方法:
extension NSUserDefaults {
func getArray<T>(key: DefaultsKey<[T]>) -> [T] {
return arrayForKey(key.key) as? [T] ?? []
}
func getArray<T>(key: DefaultsKey<[T]?>) -> [T]? {
return arrayForKey(key.key) as? [T]
}
}
|
複製、粘貼,而後參照下面這段代碼改寫全部咱們感興趣的類型:
extension NSUserDefaults {
subscript(key: DefaultsKey<[String]?>) -> [String]? {
get { return getArray(key) }
set { set(key, newValue) }
}
}
|
如今能夠這樣調用:
let key = DefaultsKey<[String]>("colors")
Defaults[key].append("red")
let red = Defaults[key][0]
|
咱們經過數組下標返回一個 String
,而後爲其添加了一個字符串,整個驗證過程發生在了編譯期(編譯器會對進行的操做進行類型檢查),這樣作更加安全便捷。
NSUserDefaults
還有一個缺點是支持的類型並很少,若是咱們想存儲自定義的類型,通用的解決辦法是用 NSKeyedArchiver
來序列化你的自定義對象。
接下來咱們努力把世界變得更美好一點,相似於 getArray
的 helper 方法,我定義了 archive()
和 unarchive()
的泛型方法,這樣我就能很容易地設計一段下標代碼來處理各類自定義類型(前提是這些類型遵循 NSCoding 協議)。
extension NSUserDefaults {
subscript(key: DefaultsKey<NSColor?>) -> NSColor? {
get { return unarchive(key) }
set { archive(key, newValue) }
}
}
extension DefaultsKeys {
static let color = DefaultsKey<NSColor?>("color")
}
Defaults[.color] // => nil
Defaults[.color] = NSColor.whiteColor()
Defaults[.color] // => w 1.0, a 1.0
Defaults[.color]?.whiteComponent // => 1.0
|
(譯者注:NSColor
遵循 NSSecureCoding
協議,而該協議繼承自 NSCoding
)
看上去並不十分完美,但咱們僅用了幾行代碼就讓 NSUserDefaults
很好地支持了自定義類型。
萬事俱備,下面有請咱們新的 API 登場:
// 提早定義鍵名
extension DefaultsKeys {
static let username = DefaultsKey<String?>("username")
static let launchCount = DefaultsKey<Int>("launchCount")
static let libraries = DefaultsKey<[String]>("libraries")
static let color = DefaultsKey<NSColor?>("color")
}
// 使用點語法來獲取 user defaults
Defaults[.username]
// 使用非可選的鍵來獲取默認值而非可選值
Defaults[.launchCount] // Int, 默認值是0
// 就地更新 value 的值
Defaults[.launchCount]++
Defaults[.volume] += 0.1
Defaults[.strings] += "… can easily be extended!"
// 使用和修改數組類型
Defaults[.libraries].append("SwiftyUserDefaults")
Defaults[.libraries][0] += " 2.0"
// 方便地使用序列化的自定義類型
Defaults[.color] = NSColor.whiteColor()
Defaults[.color]?.whiteComponent // => 1.0
|
但願你已經看到這種靜態類型帶來的好處,咱們只付出了很小的代價,包括提早定義 DefaultsKey
,聽從 Swift 的類型系統。而做爲回報,編譯器向咱們獻上一份大禮:
.string
或手動對 AnyObject
進行類型轉換Defaults
看上去更像是一個定義了類型的字典這裏還有一個潛在優點:能夠自動享受到從此 Swift 的發展紅利。
真正的 Swift 的 API 也利用了靜態類型特性,這裏不是要教條主義,條條大路通羅馬,確定還有其餘的最佳解決方案。但當你決定回到 Objective-C 或 JavaScript 的編碼習慣時,從新考慮一下靜態類型所帶來的好處,還要明白一點,這種靜態類型不是你前輩所熟悉的靜態類型,Swift 豐富的類型系統容許你創造出極具表現力和易用的 API,而實現這一切的開銷卻能夠忽略不計。
一如既往,我將以上全部的探索整理成了一個庫,放在 GitHub 上,若是感興趣,能夠採起下面的方式引用:
# with CocoaPods:
pod
'SwiftyUserDefaults'
# with Carthage:
github
"radex/SwiftyUserDefaults"
|
一樣也鼓勵你去試用我改造的另外一個 Swifty API(NSTimer),關於如何清晰命名請看我這篇文章 Swifty methods。