#Network Evolution 這是之前聽一個前FB工程師給的talk,有關網路架構的設計。因為覺得有趣且實用,方便管理且容易測試,於是在一個下雨天寫了這一篇筆記。
此筆記一共分為7個階段
- Make Requset With Alamofire
- 讓 NetworkClient 來負責網路的處理
- 測試
- API-Specific Methods
- Model 是要可以從 JSON 被解析出來的
- Service Object to Encapsulate Network Requests
- Callback -> Promise
分別講解每一個階段所做的不同的事情與變動,以及以我的角度去理解,每一個階段做出的改變的優點。
這個repo包含每一個階段的程式碼,可以直接切換到不同的commit來閱讀以下每一個階段的筆記。
code 是轉自 Austin Feight 的 Evolution of a Network Layer
我並不擁有這份 code。
一開始我們先回顧一下,我們一般在 iOS 中處理網路請求時的狀況。 一般而言,在iOS寫網路的時候大部份的我們會用callback或者delegate來處理async的資料問題。
callback
的好處是你可以隨時隨地呼叫他,但是缺點就是如果在callback中呼叫callback,那麼你的程式碼就會越來越難維護,會變成callback hell。而且容易造成dead lock跟memory leak等等問題。
delegate
也是一個不錯的方式,處理得當的話,不會有callback會造成的問題。但他的自由度沒有像callback那樣自由,他可能要符合宣告的規範,才能被使用。
以下我們使用callback來做為我們開始:
今天我們會去某一個網址,抓取我們要的username並且顯示在畫面上, 我們可以這樣做,在viewDidLoad時發送一個請求,像是這樣:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let url = "http://httpbin.org/post"
let params = ["param": "yoxisem544"]
// make a request
request(.POST, url, parameters: params).response { _, _, data, error in
if let jsonData = data where error == nil {
let json = JSON(data: jsonData)
self.label.text = "Username: " + json["form"]["param"].stringValue
} else {
self.label.text = "Requset failed"
}
}
}
這邊我們使用Alamofire
來幫我們處理網路的部份,並且在成功之後更新label上的文字。
可以看到上面我們在viewDidLoad中做了網路處理,網路處理所需要的資訊以及回傳的資料處理都暴露在外面。如果我們在view controller中需要處理越來越多request,而且如果這些requset開始交雜在一起之後,就會變得越來越難維護。
當網路連線處理越來越多,越來越難管理時,我們可以把網路處理的事情交給 NetwokrClient
來處理。
我們需要NetwokrClient
幫我們做一些事情,但相對的我們必須給他url
, params
,以及一個相對應的callback
。
struct NetworkClient {
static func makeRequest(url: String,
params: [String : AnyObject],
callback: (JSON?, ErrorType?) -> Void) {
request(.POST, url, parameters: params).response { _, _, data, error in
if let jsonData = data where error == nil {
let json = JSON(data: jsonData)
callback(json, nil)
} else {
callback(nil, error)
}
}
}
}
我們將剛剛在 view controller 中,發送連線請求的code搬到 NetworkClient
中,讓處理網路的事情,全權交給他處理。
回到 view controller 中,我們可以將剛剛的 request 改成:
NetworkClient.makeRequest(url, params: params) { json, error in
if let json = json where error == nil {
self.label.text = "Username: " + json["form"]["param"].stringValue
} else {
self.label.text = "Requset failed"
}
}
是不是稍微變簡潔一點了?
但相對我們還是有些資訊暴露在外面,而且callback回來的資料還是要稍加處理之後才可以使用,這邊可以在改進。但下一步我們會先講解測式的部份,確定測式是好維護而且有意義的之後我們才會再回來改善NetwokrClient
。
寫測試是很重要的事情,不管是 UI, Model 或者是網路處理,皆需要寫測試來驗證程式執行的正確性。
但是網路處理難的部份就是在,網路連線是asynchronous的,我們無法預期他什麼時候完成,然後會花多少的時間完成。
這時候我們就需要來模擬他所有錯誤及成功的狀態了。只要確定錯誤跟成功的狀態都如我們所預期,這個測試就會有意義了!
這邊我們要模擬出兩種網路狀態:
- 有網路
- 沒有網路
具體怎麼做呢?
首先要先建立一個protocol
:NetworkClientType
,這個型態的protocol要實做makeRequest這個方法。
protocol NetworkClientType {
static func makeRequest(url: String,
params: [String : AnyObject],
callback: (JSON?, ErrorType?) -> Void)
}
然後讓剛剛的 NetworkClient
也遵從(conform) NetworkClientType
這個協定�(protocol)。
我們要開始寫我們的測試了!這邊要新增一個 ViewControllerTests
。
根據我們剛剛的 NetworkClientType
protocol,我們可以假造兩個我們所預期的網路狀態。
private struct MockSuccessNetworkClient: NetworkClientType {
private static func makeRequest(url: String, params: [String : AnyObject], callback: (JSON?, ErrorType?) -> Void) {
let json = JSON(["form": ["params": "yoxisem544"]])
callback(json, nil)
}
}
private struct MockFailureNetworkClient: NetworkClientType {
private static func makeRequest(url: String, params: [String : AnyObject], callback: (JSON?, ErrorType?) -> Void) {
callback(nil, NSError(domain: "", code: -1, userInfo: nil))
}
}
我們新增兩個假的NetworkClient
來模擬兩個網路狀態,而且可以看到,只要conform NetworkClientType
,我們就必須實做makeRequset
這個方法。再來,我們直接在callback中回傳假資料,來假造出假的網路狀態。
最後我們回到測試,我們要來看看我們所造假的網路狀態是不是跟我們所預期的一樣。
我們要先讓test可以將vc中的networkclient取代掉,所以我們必須把protocol中的方法改成不是static的方法:
protocol NetworkClientType {
func makeRequest(url: String,
params: [String : AnyObject],
callback: (JSON?, ErrorType?) -> Void)
}
然後要先把 view controller 中的 NetworkClient 改成一個變數,這樣我們就可以在測試中賦予他我們所假造的 NetworkClient 了:
var networkClient: NetworkClientType = NetworkClient()
回到測試中,我們要開始寫我們的測試了
這邊要注意的是,view Controller 在測試中不是在模擬器或者裝置上執行,他上面的view不會被render出來,所以我們要用viewController.loadViewIfNeeded()
強迫他將畫面render出來。
而且可以注意到,我們可以將 networkClient 抽換成我們所預期的網路狀態,然後測試 ui 上所顯示的是不是我們所預期的:
extension ViewControllerTests {
func test_successNetworkResponse_showsUsername() {
viewController.networkClient = MockSuccessNetworkClient()
viewController.loadViewIfNeeded()
XCTAssertEqual(viewController.label.text, "Username: yoxisem544")
}
func test_failureNetworkResponse_showsUsername() {
viewController.networkClient = MockFailureNetworkClient()
viewController.loadViewIfNeeded()
XCTAssertEqual(viewController.label.text, "Request failed")
}
}
一切就緒後就按下 cmd + u
來執行測試吧!
上面的方法makeRequest會讓閱讀的人不清楚他主要的目的,所以我們要進一步包裝我們的API。進一步包裝過的API可以清楚的讓其他人知道他存在的目的,而且我們可以將處理資料的部分交由他處理,最後讓callback回傳一個已經包裝過且有意義的資料,讓我們可以直接使用不需要經過轉換。
首先我們要先修改 NetworkClientType
,在這個protcol加入一個新的方法叫做fetchUsername
,然後他會回傳Username
:
protocol NetworkClientType {
func fetchUsername(callback: (String?, ErrorType?) -> Void)
func makeRequest(url: String,
params: [String : AnyObject],
callback: (JSON?, ErrorType?) -> Void)
}
修改完這個protocol後,我們的NetworkClient
就會報錯,叫我們要實做fetchUsername
這個方法。
以下:
func fetchUsername(callback: (String?, ErrorType?) -> Void) {
let url = "http://httpbin.org/post"
let params = ["param": "yoxisem544"]
makeRequest(url, params: params) { (json, error) in
if let json = json where error == nil {
let username = json["form"]["param"].string
callback(username, nil)
} else {
callback(nil, error)
}
}
}
可以看到上面的callback只會回傳一個有意義的username,我們就不用在 view controller 中處理資料了!這樣也可以降低打錯字的機會。
我們把很多複雜的東西,譬如param, url都藏進NetworkClient
中了
於是在VC中我們只要單純的呼叫fetchUsername
即可:
// make a request
networkClient.fetchUsername { (username, error) in
if let username = username where error == nil {
self.label.text = "Username: " + username
} else {
self.label.text = "Request failed"
}
}
如果今天這個 API 單純只有抓取 username 並且回傳,那不需要寫成 model,但如果今天你的 model 很複雜,且擁有巢狀結構,那勢必就要寫成 model 了。寫成 model 之後,我們在取值就會變的方便而且比較不容易出錯,不需要像以前一樣還要輸入 json 的 key,一個不小心就會取不到值(nil)。
但從網路上抓下來的 JSON 並不能直接被轉成 model,中間要經過一層轉換才行。
我們來建立一個 User,這個 model 目前只包含使用者的名稱(name):
struct User {
let name: String
}
我們希望這個User能從 JSON 被轉換出來,所以我們要定義一個 protocol,只要 conform 這個 protocol 的 model 都必須實做 init?(json: JSON)
這個方法,來確保這個 model 一定可以從 JSON 被轉換出來。
protocol JSONDecodable {
init?(json: JSON)
}
回到User
,現在要讓User
conform to JSONDecodable
protocol,並且實做 init?(json: JSON)
這個方法。
extension User : JSONDecodable {
init?(json: JSON) {
guard let name = json["form"]["param"].string else { return nil }
self.name = name
}
}
因為我們已經有了 JSONDecodable
這個protocol,所以我們可以修改一下NetworkClient
,讓他的回傳(Response
)conform to JSONDecodable
這個 protocol。之後只要指定 makeRequest 的 Response,所有遵從 JSONDecodable
的資料型態皆可以交給他做網路處理。
目前我們不需要fetchUsername這個方法,所以我們先移除他。
然後回過頭來看 NetworkClientType
的 makeRequest
這個方法:
protocol NetworkClientType {
func makeRequest(url: String,
params: [String : AnyObject],
callback: (JSON?, ErrorType?) -> Void)
}
我們可以看到callback回傳的是JSON?
,現在我們希望他回傳一個Response
,而且這個Response
遵從JSONDecodable
協定。
於是我們可以把他寫成泛型
的樣子,傳入任何 conform to JSONDecodable
的 model,然後 callback 就會回傳一個如果們所預期的 Response
:
protocol NetworkClientType {
func makeRequest<Response: JSONDecodable>(url: String,
params: [String : AnyObject],
callback: (Response?, ErrorType?) -> Void)
}
因為NetworkClientType
協定變動了的緣故,所以 NetworkClient
我們也要稍加修改:
func makeRequest<Response : JSONDecodable>(url: String,
params: [String : AnyObject],
callback: (Response?, ErrorType?) -> Void) {
request(.POST, url, parameters: params).response { _, _, data, error in
if let jsonData = data where error == nil {
let json = JSON(data: jsonData)
let response = Response(json: json)
callback(response, nil)
} else {
callback(nil, error)
}
}
}
注意到 Respnse
,因為他 conform to JSONDecodable
protocol,所以他一定有一個init?(json: JSON)
的建構子可以根據 json 建構出 Response
。
因為我們把fetchUsername拿掉的緣故,現在的View Controller會有問題。
我們要回到沒有API-Specific的方法來處理我們的request:
networkClient.makeRequest(url, params: params) { (JSONDecodable?, ErrorType?) in
// code here...
}
注意看我們的 makeReqeust
的 callback 會回傳 JSONDecodable
型態的 Response。所以這邊我們必須指定這個 Response 為 User
這個型態。
networkClient.makeRequest(url, params: params) { (user: User?, error) in
// code here...
}
complete:
// make a request
networkClient.makeRequest(url, params: params) { (user: User?, error) in
if let user = user where error == nil {
self.label.text = "Username: " + user.name
} else {
self.label.text = "Request failed"
}
}
這樣處理就可以不用每次都要用 json["username"].string 去取值,也不會不小心打錯 KEY 而取到 nil。
因為我們已經修改了makeRequest的方法,所以測試的部份我們也要微調一下。
現在我們來看一下 MockSuccessNetworkClient:
private struct MockSuccessNetworkClient: NetworkClientType {}
其實只要簡單的調整一下即可:
private struct MockSuccessNetworkClient: NetworkClientType {
private func makeRequest<Response : JSONDecodable>(url: String, params: [String : AnyObject], callback: (Response?, ErrorType?) -> Void) {
let json = JSON(["form": ["param": "yoxisem544"]])
let response = Response(json: json)
callback(response, nil)
}
}
private struct MockFailureNetworkClient: NetworkClientType {
private func makeRequest<Response : JSONDecodable>(url: String, params: [String : AnyObject], callback: (Response?, ErrorType?) -> Void) {
callback(nil, NSError(domain: "", code: -1, userInfo: nil))
}
}
一樣跑一下測試~~
我們前面把 makeRequest 變成泛型後,我們就要再次把 fetchUser 拉回來了。 fetchUser 可以說是包裝過後的 network request,他專門處理 fetch user 這件事。
在包裝前,我們要先製作一個協定,讓所有的network request都遵從這個協定。他可以幫助我們列出並且做必要的事:
我們需要有一個泛型的型態associatedtype ResponseType
,我們可以指定這個protocol的回傳型態。
associatedtype ResponseType
以下是call API滿常會用到的一些東西:
必要
- endpoint: API溝通的端點。
- responseHandler: NSData -> ResponseType? :傳入
NSData
並且會回傳指定的ResponseType
上面的 responseHandler 比較特別,我們定義了這個新的 protocol,我們會希望這個 protocol 有人可以幫我們處理轉換 model 這件事。而這個handler就可以幫我們定義這件事。這個 responseHandler 需要傳入一個 NSData
然後處理完畢之後回傳 ResponseType?
。
非必要
- baseUrl: 固定的連線網址
- method: Alamofire.Method RESTful API methods, ex. GET, POST
- encoding
- params: 參數
- headers
- networkClient: 處理網路連線的client
我們希望遵從NetworkRequest
協定的request都包含以上條件,可以方便我們發送request。
protocol NetworkRequest {
associatedtype ResponseType
// Required
var endpoint: String { get }
var responseHandler: NSData -> ResponseType? { get }
// Optional
var baseURL: String { get }
var method: Alamofire.Method { get }
var encoding: Alamofire.ParameterEncoding { get }
var parameters: [String : AnyObject] { get }
var headers: [String : String] { get }
var networkClient: NetworkClientType { get }
}
一般在做api call時,我們都需要用baseURL跟endpoint串成一個有意義的url來做request。而baseURL就是剛剛的http://httpbin.org/post
。
extension NetworkRequest {
var url: String { return baseURL + endpoint }
var baseURL: String { return "http://httpbin.org/" }
var method: Alamofire.Method { return .GET }
var encoding: Alamofire.ParameterEncoding { return .JSON }
var parameters: [String : AnyObject] { return [:] }
var headers: [String : String] { return [:] }
var networkClient: NetworkClientType { return NetworkClient() }
}
如果今天這個 ResponseType
遵從 JSONDecodable
協定,那麼他就可以從 JSON 被解析,因此我們可以對他做一點特別的事情。我們來定義一個特別的方法專門來處理他:
private func jsonResponseHandler<Response: JSONDecodable>(data: NSData) -> Response? {
let json = JSON(data: data)
return Response(json: json)
}
他的型態有點像上面的responseHandler: NSData -> ResponseType?
,但這樣還不夠,我們要擴展 NetworkRequest
然後只有在 ResponseType
conform to JSONDecodable
時才有的特別方法,而且可以把 responseHandler 抽換成我們剛剛定義的 jsonResponseHandler
:
extension NetworkRequest where ResponseType: JSONDecodable {
var responseHandler: NSData -> ResponseType? { return jsonResponseHandler }
}
這樣我們就處理完NetworkRequest
這個協定了!
因為我們現在有 NetworkRequest
這個協定,所以我們現在要稍微修改一下 NetworkClientType
協定。
我們希望讓NetworkClientType
專注處理request就好,並不需要傳url, params等等資訊。
這邊我們要傳給NetworkClientType
的只有單純的NetworkRequest
即可,因為裡面有我們所需要的資訊。
所以我們稍加修正變成這樣:
protocol NetworkClientType {
func makeRequest<Request: NetworkRequest>(networkRequest: Request, callback: (NSData?, ErrorType?) -> Void)
}
NetworkClientType
一變動,勢必也要更改NetworkClient
。
回想一下剛剛的NetworkRequest
已經可以處理JSON的事情,所以NetworkClient
只負責網路處理的部份,他並不處理資料。
而且NetworkClient
也被包進NetworkRequest
,變成處理連線的一小部份了。之後想做網路連線,只要遵從NetworkRequest
即可。
於是我們可以把NetworkClient
改成這樣:
func makeRequest<Request : NetworkRequest>(networkRequest: Request, callback: (NSData?, ErrorType?) -> Void) {
request(networkRequest.method,
networkRequest.url,
parameters: networkRequest.parameters,
encoding: networkRequest.encoding,
headers: networkRequest.headers)
.response { (_, _, data, error) in
if let data = data where error == nil {
callback(data, nil)
} else {
callback(nil, error)
}
}
}
我們終於要來包裝我們的API了~
我們現在要建立一個物件,他專門處理fetch user這件事,而且要遵從NetworkRequest
這個協定。
class FetchUser: NetworkRequest {}
然後我們必須指定我們的回傳型態: User
,然後指定一些endpoint跟params等等
class FetchUser: NetworkRequest {
typealias ResponseType = User
var endpoint: String { return "post" }
var method: Alamofire.Method { return .POST }
var parameters: [String : AnyObject] { return ["param": username] }
private var username: String = ""
}
這些資訊一直以來都暴露在view controller之中,但這些資訊都不是我們需要知道的事情,所以最好藏著比較好。
接著我們要包裝出一個可以在view controller之中方便取用而且名字具有意義的方法,我們就把他叫做:
perform(username: String, callback: (User?, ErrorType?) -> Void)
只需要傳入使用者的名稱,他就會回傳一個User
回來。
實做:
func perform(username: String, callback: (User?, ErrorType?) -> Void) {
self.username = username
let parsedCallback = { (data: NSData?, error: ErrorType?) in
let response = data.flatMap(self.responseHandler)
callback(response, error)
}
networkClient.makeRequest(self, callback: parsedCallback)
}
因為現在的makeRequest
只能傳入NetworkRequest
型態的資訊,且會回傳一個帶有NSData?
的callback,這邊我們就需要改變一下callback的型態。
回想一下,我們的NetworkRequest
具有一個幫我們處理response的responseHandler
,這邊會需要傳入NSData,然後這個responseHandler
會幫我們處理然後回傳ResonseType
。其實我們的ResonseType
剛剛被我們指定為User
,所以這邊會觸發jsonResponseHandler
進而直接幫我們把NSData
轉成User
,因為User
是JSONDecodable
。
所以
let response = data.flatMap(self.responseHandler)
的response
其實已經是User
型態了,因為上面有指定ResponseType
= User
。
到此,我們已經完成FetchUser這個Object了。
因為我們已經有FetchUser
這個Object了,所以我們可以直接把networkClient取代掉。
var fetchUser: FetchUser = FetchUser()
然後:
// make a request
fetchUser.perform("yoxisem544") { (user, error) in
if let user = user where error == nil {
self.label.text = "Username: " + user.name
} else {
self.label.text = "Request failed"
}
}
因為剛剛把encoding改成JSON,這邊還要改一下User
:
struct User {
let name: String
}
extension User : JSONDecodable {
init?(json: JSON) {
guard let name = json["json"]["param"].string else { return nil }
self.name = name
}
}
最後就執行看看吧。
我們來修改一下 Mock 部份:
private class MockSuccessFetchUser: FetchUser {
private override func perform(username: String, callback: (User?, ErrorType?) -> Void) {
let user = User(name: username)
callback(user, nil)
}
}
private class MockFailureFetchUser: FetchUser {
private override func perform(username: String, callback: (User?, ErrorType?) -> Void) {
callback(nil, NSError(domain: "", code: -1, userInfo: nil))
}
}
我們現在需要假造的是perform這個方法。
然後改一下test method:
extension ViewControllerTests {
func test_successNetworkResponse_showsUsername() {
viewController.fetchUser = MockSuccessFetchUser()
viewController.loadViewIfNeeded()
XCTAssertEqual(viewController.label.text, "Username: yoxisem544")
}
func test_failureNetworkResponse_showsUsername() {
viewController.fetchUser = MockFailureFetchUser()
viewController.loadViewIfNeeded()
XCTAssertEqual(viewController.label.text, "Request failed")
}
}
一樣跑一下測試!
太多的callback大家應該都知道code會變的很髒很亂吧? 所以才衍伸出Promise這個概念,請自行Google。
fb的Bolts也有點類似promise。(應該啦)
這邊我們不要回傳callback,我們要讓這個協定變成回傳Promist。
protocol NetworkClientType {
func performRequest<Request: NetworkRequest>(networkRequest: Request) -> Promise<NSData>
}
這邊是我們處理callback的地方,我們可以把這邊的callback包裝下,變成promise。
Promise<NSData>.pendingPromise()
會回傳三個東西
- promise
- success
- failure
然後pendingPromise
可以不用立馬執行。
然後讓我們修改一下NetworkClient
:
func performRequest<Request : NetworkRequest>(networkRequest: Request) -> Promise<NSData> {
let (promise, success, failure) = Promise<NSData>.pendingPromise()
request(networkRequest.method,
networkRequest.url,
parameters: networkRequest.parameters,
encoding: networkRequest.encoding,
headers: networkRequest.headers)
.response { (_, _, data, error) in
if let data = data where error == nil {
success(data)
} else if let error = error {
failure(error)
}
}
return promise
}
很簡單,只需要把要反應的地方加上promise即可。
我們也要將FetchUser
的callback改成promise
func perform(username: String) -> Promise<User> {}
然後這邊我們需要先改一下model的錯誤處理,一開始我們都是用init?(json: JSON)
來init,然後檢查是不是optional。現在我們改用Promise,他必須使用throws來處理錯誤,所以我們要改以下這些東西:
JSONDecodable:
protocol JSONDecodable {
init(json: JSON) throws
}
User:
struct User {
let name: String
}
extension User : JSONDecodable {
init(json: JSON) throws {
guard let name = json["json"]["param"].string else { throw JSONError.MissingKey("json.param") }
self.name = name
}
}
NetworkRequest:
protocol NetworkRequest {
associatedtype ResponseType
// Required
var responseHandler: NSData throws -> ResponseType { get }
}
extension NetworkRequest where ResponseType: JSONDecodable {
var responseHandler: NSData throws -> ResponseType { return jsonResponseHandler }
}
private func jsonResponseHandler<Response: JSONDecodable>(data: NSData) throws -> Response {
let json = JSON(data: data)
return try Response(json: json)
}
接著我們回到FetchUser,準備要來修改perform這個方法了。
因為networkClient的promise的緣故,他會回傳NSData到then這個方法裡面,而剛好就可以直接交給responseHandler
處理了。
接個按著option點擊then這個方法,可以看到他會throws,所以這也是剛剛為什麼要改model的原因。
以下就是串上promise的perform!
func perform(username: String) -> Promise<User> {
self.username = username
return networkClient.performRequest(self).then(responseHandler)
}
修改剛剛我們所變更的東西
// make a request
fetchUser.perform("yoxisem544")
.then { user in
self.label.text = "Username: " + user.name
}
.error { error in
self.label.text = "Request failed"
}
接著執行程式。
private class MockSuccessFetchUser: FetchUser {
private override func perform(username: String) -> Promise<User> {
return Promise(User(name: username))
}
}
private class MockFailureFetchUser: FetchUser {
private override func perform(username: String) -> Promise<User> {
return Promise(error: NSError(domain: "", code: -1, userInfo: nil))
}
}
然後好像是因為Promise緣故,所以很多東西都會跑進queue中,所以在測試的方法中,我們必須稍微等待他執行。
extension ViewControllerTests {
func test_successNetworkResponse_showsUsername() {
viewController.fetchUser = MockSuccessFetchUser()
viewController.loadViewIfNeeded()
let expectation = expectationWithDescription("Label set")
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10), dispatch_get_main_queue()) {
XCTAssertEqual(self.viewController.label.text, "Username: yoxisem544")
expectation.fulfill()
}
waitForExpectationsWithTimeout(10, handler: nil)
}
func test_failureNetworkResponse_showsUsername() {
viewController.fetchUser = MockFailureFetchUser()
viewController.loadViewIfNeeded()
let expectation = expectationWithDescription("Label set")
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10), dispatch_get_main_queue()) {
XCTAssertEqual(self.viewController.label.text, "Request failed")
expectation.fulfill()
}
waitForExpectationsWithTimeout(10, handler: nil)
}
}
接著在跑一下測試。
我花了一整天讀這份code,然後一段一段寫筆記,媽呀真的有夠累的。
這是 Austin 在 Cocoaheads 上給的 talk,最近終於有時間來細讀了!感謝 Austin Feight 提供這麼棒的觀念!!!
Many thanks to Austin Feight!
以上的 code 皆是轉自 Austin Feight 的 Evolution of a Network Layer