Swift開發的幾個小技巧

edithfang發表於2015-02-04
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 思維改造專案的過程中,目前這種模式應該還是有改進餘地的,希望能和大家做更多有關這個的討論與交流。
相關閱讀
評論(1)

相關文章