Swift開發的幾個小技巧
正所謂掌握一樣技術***的辦法就是用它來做一個東西,于是這段時間的實戰讓我對 Swift 的理解更深了一層,也積累了一些使用技巧。今天就分享一則:如何正確地定義一個類變量(和類常量)。
Swift 語言對于無論是靜態語言過來還是動態語言過來的開發者來說,都有點點不適應,很多問題的解決思路不能用已經習以為常的方法去做。
如何正確的定義一個類變量(和類常量)
Swift 支持用 class func 來修飾一個「類方法」,然而卻不能用「class var」和「class let」來指定類變量和類常量,一旦你嘗試這樣做了,那么 Xcode 會提示你:Class variable not yet supported。真是遺憾…
不過從這個提示可以看出,Class variable 的支持只是時間問題。那么現階段我們要怎么完成這一目標?總不能用丑陋的 Workaround 吧。辦法還是有的,我從 Apple 官方的例子中學到了如何去定義一個類級的常量和變量,那就是用 struct。
來個 demo 你就明白了:
- class MyClass {
- struct Constants {
- static let name = "MyClass"
- }
- struct Variables {
- static var age = 0
- }
- }
然后在調用的時候,就可以這樣來調用: MyClass.Constants.name 和 MyClass.Variables.age
雖 然中間還隔了一層 Constants 和 Variables,但是我覺得這樣也挺好,相當于有了一個 Prefix,直接看代碼時就知道是常量還是變量了。如果你不喜歡這種方式,也可以用 computed property 的形式來模擬真實的類變量(常量)的調用。比如:
- extension MyClass {
- class var name: String {
- get {
- return Constants.name
- }
- }
- class var age: Int {
- get {
- return Variables.age
- }
- set {
- Variables.age = newValue
- }
- }
- }
定義了這種方式后,就可以直接用 MyClass.name 和 MyClass.age 來訪問類常量或修改類變量了。
這種方式在語法上兼容了未來會得到支持的類變量和類常量,但就是自己要寫一大堆 getter 和 setter,有點麻煩,大家可以根據自己的需要決定是不是要采用這種方式。
關于本文的 demo 代碼,大家可以粘貼進「swift」這個命令行工具來實踐一下。效果正如我們想要的那樣,常量不允許修改,變量可以修改,所有的這些操作都是在 MyClass 上進行,而不需要實例化。
雖然現在用 Swift 來做一些常用的任務還略顯麻煩,不過作為一個年輕的語言,目前它確實已經能用在生產環境中寫出真正可用的 App 了,隨著接下去的發展,我相信它會變得越來越好的。
PS:現在我只想 SourceKitService 崩潰的少一點…
用Optional來避免異常指針問題
最近在用 Swift 開發的過程中,又碰到了一個問題。簡單的說,系統在該返回非 nil 值的地方返回了一個異常指針(即指向 0x0000 地址,產生 KERN_INVALID_ADDRESS 異常)造成了 App 的 crash,算是 iOS UIKit 的一個 Bug。
這個問題需要 SDK 的升級來解決,但是在 SDK 升級之前,我們可以通過一個小小的 Workaround 去解決。來龍去脈是這樣的:
iOS 的 Swift 版的 API 與 Objective-C 的 API ***的不同是,你需要看 Objective-C 的 API 文檔才知道一個系統返回的值是不是可能是 nil。
比如 UIDataSourceModelAssociation 這個用來還原 UITableView 和 UICollectionView 位置的 Protocol,它有兩個方法,其中一個是:
- 1
- - (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view
- 光看 Objective-C 這個方法,你是不知道系統返回的 idx 有沒有可能是 nil 的,但是一看 Swift 版本,就非常顯然了:
- 1
- func modelIdentifierForElementAtIndexPath(_ idx: NSIndexPath, inView view: UIView) -> String
- idx 和 inView 一樣,都沒有 ? 和 !,因而它們不是 Optional,所以值不可能是 nil。如果 idx 和 inView 可能是 nil 的,那么它應該是用 ! 來修飾,以警告開發者,這個值可能是 nil,請小心使用。
- 1
- func modelIdentifierForElementAtIndexPath(_ idx: NSIndexPath!, inView view: UIView!)
Swift 這種比 Objective-C 更徹底的 API 即文檔的表達形式,我在編碼一段時間后非常喜歡。
然而,也正是系統的框架還進化的不徹底的原因,這些 API 依然可能會返回 nil 的值,但是 API 因為被標記不會返回 nil,于是就會發生 swift_dynamicCastClassUnconditional 的異常,***導致 App crash。
下圖即是 modelIdentifierForElementAtIndexPath 這個方法在應該返回值的情況下,卻返回了一個指向 0x0000000000000000 的對象。然后 Swift 在包裝值的時候,沒能包裝成功而發生 crash。
那么如何避免因為這個問題造成的 App crash 呢?實際上很簡單,只需要手動用 Optional 包裝一下這個 0x0000 對象,再判斷它是不是 nil,就不會發生問題了。比如這樣的代碼:
- let optionalIdx = Optional(idx)
- if optionalIdx == nil {
- return "Do something"
- }
剛開始遇到這個問題我也很苦惱,后來突然想到,是不是可以用 Optional 去包裝一下這種異常指針再檢查是不是 nil,一試果然可以。于是問題就這樣解決了。
幸好,這種坑沒有多到讓我抓狂的步地,我能繼續用 Swift 愉快的寫下去了…
如何用 Swift 思維設計網絡請求
近來在用 Swift 開發 App 的過程中,***的心得就是:我開始漸漸用「Swift 思維」來思考了。回顧剛開始我用 Swift 時,只是套用它的語法而已,腦子里依然是 Objective-C 思維。
這段時間,隨著對 Swift 基本特性的掌握,我開始有意識地學習并嘗試一些 Swift 才有的特性,此謂「Swift 思維」。Swift 有很多專有(Objective-C 沒有的)的模式,今天我就從一個很簡單的例子講起,那就是:
如何用 Swift 思維設計網絡請求。
做過網絡類應用的同學應該都知道,我們做一個網絡請求時,通常會有兩個結果:一個是失敗,返回錯誤,一個是成功,返回結果。當然途中還會有更復雜的情況,比如:1、網絡請求本身的失敗(比如網絡超時);2、API 端返回的內部結果型失敗(比如密碼不正確)。這里我們就不細分了。
在傳統的 Objective-C 的項目里,有幾種處理這種異常的情況,主要是:
直接判斷 NSError
- NSError *error = nil;
- id result = [API doSomething:&error];
- if (error != nil) {
- NSLog("Oh Error!")
- }
這種處理太直白,一般會阻塞當前的線程,因而不推薦。一般用的比較多的是下面兩種:
通過 success,failture 的 block 來處理
- [API doSomethingWithSuccess:^(id result) {
- NSLog(@"Seems good")
- } failure:^(NSError *error) {
- NSLog(@"Oh Error!")
- }];
這種就相對好點了,通過 Block 及內部實現,可以做到不阻塞當前線程,在有結果的時候再進行處理。在對應的 Block 處理對應的情況:成功 or 失敗。 不過這個設計依然還有一點缺陷,因為它對結果的處理分散在不同的 Block,如果我需要統一處理無論成功或失敗的情況,那么需要分別調用,不是太直觀了。所以,我們還有第三種模式。
通過統一的 completionHander 的 block 來處理
- [API doSomethingWithCompletionHandler:^(id result, NSError *error) {
- if result != nil {
- NSLog(@"Seems good")
- }
- if error != nil {
- NSLog(@"Oh Error!")
- }
- }];
這種通過 CompletionHandler 來統一作成功結果和錯誤失敗的處理應該是現在設計的首先,包括系統自己的 API 也是這樣設計的。特別適合在一個 Block 里就把所有情況處理掉的需求。
Swift 式網絡請求處理
簡單列舉了三種 Objective-C 下常見的網絡請求類處理方式,看起來還不錯,那么 Swift 模式是什么樣的,能做好更好嗎?我覺得是的。
Swift 里有著非常棒的 enum 機制,所有的枚舉情況不但可以是任何類型,而且可以是不一樣的類型。這意味著,我們在 Swift 里可以包裝一種結果型 enum,比如:
- enum Result {
- case Error(NSError)
- case Value(JSON)
- init(_ e: NSError?, _ v: JSON) {
- if let ex = e {
- self = Result.Error(ex)
- } else {
- self = Result.Value(v)
- }
- }
- }
這段是我真實世界的代碼,用在了我的微博客戶端里。
代碼很簡單,我定義了一個名為「Result」的 enum 對象,它會包裝兩種情況,一種是 Value,在網絡請求成功時,它就是一個 JSON 值;第二種時 Error,是一個 NSError 值,在網絡請求失敗時,包含著具體的錯誤信息。
這樣,就成功地把一個網絡請求下的可能的兩種情況包裝在了一個 Result 對象里,這個對象,要么是成功的結果,要么就是失敗的錯誤,永遠不會有同時有結果和錯誤。于是,我們的網絡請求處理代碼可以更為簡單的設計成這樣:
- API.doSomethingWithCompletionHandler({ (result) -> Void in
- switch (result) {
- case let .Error(e):
- NSLog("Oh Error!")
- case let .Value(json):
- NSLog("Seems good")
- }
- })
看起來似乎和前面 Objective-C 的第二種模式一樣?似乎又像第三種?估且稱之為混合模式吧。讓我來簡單說說這種模式有什么好處:
首先,我們通過 Switch 條件判斷這種非此即彼的模式,我們可以減少很多錯誤的發生,保證條件分支判斷不會出問題;其次,我們依然只是在一個 Closure (這里換成 Swift 術語,而不是 Objective-C 的 Block)處理我們的一個請求結果 result,因而可以在前前后后做些其他對結果的統一處理,保證我們邏輯的統一性。
這就是我所認為的 Swift 這種模式的好處了。
通過這種模式改造我的項目后,我覺得代碼變得更整潔、邏輯清晰,也不會有遺失的錯誤處理情況。當然我做采用的還只是簡化版的 Swift 模式網絡處理,更為強大的例子大家可以參考 swiftz 項目的 Result 對象,它使用了 Swift 的 Generic 特性,從而使其可以包裝任意值(不僅僅是 JSON),從而大大增強了擴展性:https://github.com/typelift/swiftz/blob/master/swiftz_core/swiftz_core/Result.swift#L12
我依然還在用 Swift 思維改造項目的過程中,目前這種模式應該還是有改進余地的,希望能和大家做更多有關這個的討論與交流。