Swift 跟 OC 有著完全不同的設計哲學,它鼓勵你使用 protocol 而不是 super class,使用 enum 和 struct 而不是 class,它支援函式式特性、範型和型別推導,讓你可以輕鬆封裝非同步過程,用鏈式呼叫避免 callback hell。如果你還是用 OC 的思維寫著 Swift 程式碼,那可以說是一種極大的資源浪費,你可能還會因為 Swift 弱雞的反射而對它感到不滿,畢竟 Swift 在強型別和安全性方面下足了功夫,如果不使用 OC 的 runtime,在動態性方面是遠不如 OC 的。
OOP 和訊息傳遞非常適合 UI 程式設計,在這方面來說 OC 是非常稱職的,整個 Cocoa Touch 框架也都是物件導向的,所以對於 iOS 開發來說,不管你使用什麼語言,都必須熟悉 OOP。在 UI 構建方面,無論是 Swift 還是 OC,無非都是呼叫 API 罷了,在有自動提示的情況下,其實編碼體驗都差不多。那 Swift 相比於 OC 的優勢到底體現在什麼地方呢,我認為是 UI 以外的地方,跟 UI 關係越小,Swift 能一展拳腳的餘地就越大,譬如網路層。
講到網路層就繞不開 Alamofire,Alamofire 幾乎是現在用 Swift 開發 iOS App 的標配,它是個很棒的庫,幾乎能滿足所有網路方面的日常需求,但如果對它再封裝一下的話,不僅使用起來更得心應手,而且能將第三方庫與業務程式碼解耦,以後萬一要更換方案會更加方便。
Alamofire 使用 Result 來表示請求返回的結果,它是個 enum,長這樣:
1 2 3 4 5 6 7 8 9 10 11 12 |
public enum Result { case Success(Value) case Failure(Error) /// Returns `true` if the result is a success, `false` otherwise. public var isSuccess: Bool { get } /// Returns `true` if the result is a failure, `false` otherwise. public var isFailure: Bool { get } /// Returns the associated value if the result is a success, `nil` otherwise. public var value: Value? { get } /// Returns the associated error value if the result is a failure, `nil` otherwise. public var error: Error? { get } } |
我們可以對它進行擴充套件,讓它支援鏈式呼叫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
import Foundation import Alamofire extension Result { // Note: rethrows 用於引數是一個會丟擲異常的閉包的情況,該閉包的異常不會被捕獲,會被再次丟擲,所以可以直接使用 try,而不用 do-try-catch // U 可能為 Optional func map(@noescape transform: Value throws -> U) rethrows -> Result { switch self { case .Failure(let error): return .Failure(error) case .Success(let value): return .Success(try transform(value)) } } // 若 transform 的返回值為 nil 則作為異常處理 func flatMap(@noescape transform: Value throws -> U?) rethrows -> Result { switch self { case .Failure(let error): return .Failure(error) case .Success(let value): guard let transformedValue = try transform(value) else { return .Failure(SYError.errorWithCode(.TransformFailed) as! Error) } return .Success(transformedValue) } } // 適用於 transform(value) 之後可能產生 error 的情況 func flatMap(@noescape transform: Value throws -> Result) rethrows -> Result { switch self { case .Failure(let error): return .Failure(error) case .Success(let value): return try transform(value) } } // 處理錯誤,並向下傳遞 func mapError(@noescape transform: Error throws -> NSError) rethrows -> Result { switch self { case .Failure(let error): return .Failure(try transform(error)) case .Success(let value): return .Success(value) } } // 處理資料(不再向下傳遞資料,作為資料流的終點) func handleValue(@noescape handler: Value -> Void) { switch self { case .Failure(_): break case .Success(let value): handler(value) } } // 處理錯誤(終點) func handleError(@noescape handler: Error -> Void) { switch self { case .Failure(let error): handler(error) case .Success(_): break } } } |
有了這個擴充套件我們就可以定義一個parseResult
的方法,對返回結果進行處理,像這樣:
1 2 3 4 5 6 |
func parseResult(result: Result, responseKey: String) -> Result { return result .flatMap { $0 as? [String: AnyObject] } .flatMap(self.checkJSONDict) // 解析錯誤資訊並進行列印,然後繼續向下傳遞,之後業務方可自由選擇是否進一步處理錯誤 .flatMap { $0.valueForKey(responseKey) } } |
checkJSONDict
用來處理伺服器返回的錯誤資訊,具體的處理邏輯不同專案都不一樣,主要看跟伺服器的約定,我就不細說了。valueForKey
是對Dictionary
的擴充套件,可以通過字串拿到返回的 JSON 資料中需要的部分(先轉換成[String: AnyObject]
),支援用”.”分隔 key,從而取得巢狀物件。譬如這樣一個東西:
1 2 3 4 5 |
{ key1: value1, key2: { nest: value2 } key3: { nest1: { nest2: value3 } } } |
你可以用"key2.nest"
拿到value2
,用"key3.nest1.nest2"
拿到value3
。我用reduce
實現了這個功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
extension Dictionary { var dictObject: AnyObject? { return self as? AnyObject } func valueForKey(key: Key) -> Value? { guard let stringKey = key as? String where stringKey.containsString(".") else { return self[key] } let keys = stringKey.componentsSeparatedByString(".") guard !keys.isEmpty else { return nil } let results: AnyObject? = keys.reduce(dictObject, combine: fetchValueInObject) return results as? Value } } func fetchValueInObject(object: AnyObject?, forKey key: String) -> AnyObject? { return (object as? [String: AnyObject])?[key] } |
有了parseResult
之後,我們就可以輕鬆封裝請求過程了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/** Fetch raw object - parameter api: API address - parameter method: HTTP method, default = POST - parameter parameters: Request parameters, default = nil - parameter responseKey: Key of target value, use '.' to get nested objects, e.g. "data.vehicle_list" - parameter jsonArrayHandler: Handle result with raw object - returns: Optional request object which is cancellable. */ func fetchDataWithAPI(api: API, method: Alamofire.Method = .POST, parameters: [String: AnyObject]? = nil, responseKey: String, networkCompletionHandler: NetworkCompletionHandler) -> Cancellable? { guard let url = api.url else { printLog("URL Invalid: (api.rawValue)") return nil } let params = configParameters(parameters) return Alamofire.request(method, url, parameters: params).responseJSON { networkCompletionHandler(self.parseResult($0.result, responseKey: responseKey)) } } |
API
是一個列舉,有一個url
的計算屬性,用來返回 API 地址,configParameters
用來配置請求引數,也跟具體專案有關,就不展開了,method
可以設定一個專案中常用的 HTTP Method 作為預設引數。這個方法會返回一個Cancellable
,長這樣:
1 2 3 4 5 |
protocol Cancellable { func cancel() } extension Request: Cancellable {} |
Request
本來就實現了cancel
方法,所以只要顯式地宣告一下它遵守Cancellable
協議就行了,使用的時候像這樣:
1 2 3 4 |
let task = NetworkManager.defaultManager .fetchDataWithAPI(.ModelList, responseKey: "data.model_list") { // ... } |
在請求完成之前,隨時可以呼叫task?.cancel()
來取消這個網路任務。
當然如果你想在網路模組中把 JSON 直接轉化成 Model 也是可以的,我個人傾向於使用 ObjectMapper 來構建網路 Model 層,於是就可以對外提供兩個直接取得 Model 和 Model 陣列的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
/** Fetch JSON model - parameter api: API address - parameter method: HTTP method, default = POST - parameter parameters: Request parameters, default = nil - parameter responseKey: Key of target value, use '.' to get nested objects, e.g. "data.vehicle_list" - parameter jsonArrayHandler: Handle result with model - returns: Optional request object which is cancellable. */ func fetchJSONWithAPI(api: API, method: Alamofire.Method = .POST, parameters: [String: AnyObject]? = nil, responseKey: String, jsonHandler: Result -> Void) -> Cancellable? { return fetchDataWithAPI(api, method: method, parameters: parameters, responseKey: responseKey) { jsonHandler($0.flatMap(=>)) } } /** Fetch JSON array - parameter api: API address - parameter method: HTTP method, default = POST - parameter parameters: Request parameters, default = nil - parameter responseKey: Key of target value, use '.' to get nested objects, e.g. "data.vehicle_list" - parameter jsonArrayHandler: Handle result with model array - returns: Optional request object which is cancellable. */ func fetchJSONArrayWithAPI(api: API, method: Alamofire.Method = .POST, parameters: [String: AnyObject]? = nil, responseKey: String, jsonArrayHandler: Result -> Void) -> Cancellable? { return fetchDataWithAPI(api, method: method, parameters: parameters, responseKey: responseKey) { jsonArrayHandler($0.flatMap(=>)) } } |
=>
是我自定義的操作符,它有兩個過載版本,都滿足flatMap
的引數要求:
1 2 3 4 5 6 7 8 9 |
postfix operator => {} postfix func =>(object: AnyObject) -> T? { return Mapper().map(object) } postfix func =>(object: AnyObject) -> [T]? { return Mapper().mapArray(object) } |
於是就可以在業務程式碼中直接這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class TableViewController: UITableViewController { // ... var results: [Demo]? { didSet { tableView.reloadData() } } func fetchData() { let task = NetworkManager.defaultManager .fetchJSONArrayWithAPI(.Demo, responseKey: "data.demo_list") { self.results = $0.value } } } |
到此一個簡潔方便的網路模組就差不多成型了,別忘了為你的模組新增單元測試,這會讓模組的使用者對你的程式碼更有信心,而且在測試過程中會讓你發現一些開發過程中的思維盲區,還能幫你優化設計,畢竟良好的可測試性在某種程度上就意味著良好的可讀性和可維護性。
有什麼建議歡迎在評論中指出 ^ ^