重要說明: 這是一個系列教程,非本人原創(chuàng)蜈缤,而是翻譯國外的一個教程。本人也在學(xué)習(xí)Swift冯挎,看到這個教程對開發(fā)一個實際的APP非常有幫助底哥,所以翻譯共享給大家。原教程非常長房官,我會陸續(xù)翻譯并發(fā)布趾徽,歡迎交流與分享。
我已準(zhǔn)備好到直接跳到了一些對你有用的代碼翰守。但是孵奶,蘋果在iOS9中引入App Transport Security,雖然ATS是保護(hù)從你iPhone中發(fā)送和接受的數(shù)據(jù)蜡峰,但對于開發(fā)人員來說的確有點頭痛了袁。
ATS要求使用SSL進(jìn)行數(shù)據(jù)傳輸,這對實施來說是非常挑剔的湿颅≡芈蹋可悲的是,現(xiàn)在很多服務(wù)器是不滿足這項要求的肖爵。所以卢鹦,如果我們需要與訪問這些服務(wù)器時該怎么解決呢?接下來我們會解決這個問題劝堪,因為后面講解網(wǎng)絡(luò)調(diào)用基礎(chǔ)知識所使用的服務(wù)器是需要先解決這個問題的冀自。
我們將為這種應(yīng)用情況下的應(yīng)用程序傳輸安全增加一個異常。雖然我們可以禁用ATS秒啦,但這樣比只為一臺服務(wù)器創(chuàng)建異常更安全熬粗。在本章我們所要訪問的API是http://jsonplaceholder.typicode.com/
。
我們需要在項目的info.plist
文件中增加一些鍵余境,以便創(chuàng)建該異常驻呐。我們會添加一個NSAppTransportSecurity
的字典,該字典中包含NSExceptionDomains
字典芳来,存放服務(wù)器:jsonplaceholder.typicode.com
(注意:沒有結(jié)尾的斜杠含末,也沒有http或https前綴)。在jsonplaceholder.typicode.com
字典中增加一個布爾值的條目NSThirdPartyExceptionAllowsInsecureHTTPLoads
即舌,并把值設(shè)置為YES
:
<div style="text-align: center">
</div>
這樣佣盒,下面我們就可以正式開始編寫代碼了。
使用Swift進(jìn)行簡單的REST API調(diào)用
當(dāng)今幾乎每一個APP都會通過API獲取或創(chuàng)建內(nèi)容顽聂。在本書中肥惭,我們會使用一個非常強(qiáng)大的網(wǎng)絡(luò)處理庫Alamofire盯仪,當(dāng)然你也可以使用NSURLSession
快速的,不是那么優(yōu)雅的執(zhí)行異步數(shù)據(jù)請求任務(wù)蜜葱。
在Swift進(jìn)行異步URL請求的方法如下:
public func dataTaskWithRequest(request: NSURLRequest,
completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void)
-> NSURLSessionDataTask
該方法將向特定的URL發(fā)起一個請求全景,請求發(fā)送完畢后將停止運行。一旦得到對方服務(wù)器的響應(yīng)(甚至是一個錯誤報告)牵囤,相應(yīng)的完成處理程序(completion handler)將被調(diào)用爸黄。我們可以在完成處理程序中對調(diào)用的結(jié)果進(jìn)行處理,包括錯誤檢查奔浅、將數(shù)據(jù)保存到本地馆纳、更新界面诗良,等等汹桦。在實現(xiàn)dataTaskWithRequest
章節(jié)我們會深入討論完成處理程序。
最簡單的例子就是一個GET
請求鉴裹。不過舞骆,我們需要一個API來進(jìn)行測試,幸運的是JSONPlaceholder可以充當(dāng)這個工具径荔。
JSONPlaceholder是一個用來測試和制作原型的在線偽REST API督禽。就像網(wǎng)頁中的圖像占位符。
JSONPlaceholder中有一些資源可以使用总处,類似于我們在很多應(yīng)用中使用的:用戶狈惫、帖子、照片鹦马、相冊...等等胧谈。不過我們這里使用帖子(posts)。
首先荸频,讓我們先打印出第一個帖子的標(biāo)題菱肖。我們可以通過發(fā)起一個到posts
端點(endpoint)的GET
請求,參數(shù)為帖子的ID
旭从,來獲取一個帖子的詳細(xì)信息稳强。從http://jsonplaceholder.typicode.com/posts/我們可以知道第一個帖子的ID
是1。下面就讓我們獲取它:
首先和悦,我們設(shè)置URL請求:
let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
guard let url = NSURL(string: postEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = NSURLRequest(URL: url)
guard
關(guān)鍵字可以讓我們檢查所請求的URL是否有效退疫。
接下來,使用NSURLSession
發(fā)送請求:
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
然后鸽素,創(chuàng)建數(shù)據(jù)任務(wù)(data task):
let task = session.dataTaskWithRequest(urlRequest, completionHandler: nil)
最后褒繁,發(fā)送請求(是的,這個方法名稱有點怪異)
task.resume()
現(xiàn)在我們將調(diào)起URL(通過urlRequest)付鹿,并獲得相應(yīng)的結(jié)果(作為默認(rèn)將使用GET
請求)澜汤。因此我們必須實現(xiàn)一個完成處理程序?qū)Ψ祷亟Y(jié)果進(jìn)行處理蚜迅,以便完成相應(yīng)的處理。
當(dāng)你第一次運行時俊抵,完成處理程序可能會有點混亂谁不。一方面,它是一個變量或參數(shù)徽诲;另一方面刹帕,它又是一堆代碼。如果你以前沒有用過這種程序(塊或者閉包)谎替,你會覺得非常奇怪偷溺。
當(dāng)你的應(yīng)用需要花費一些時間進(jìn)行任務(wù)處理,比如:API調(diào)用钱贯,并且在任務(wù)處理完畢后需要執(zhí)行某些動作處理挫掏,如更新界面顯示新的數(shù)據(jù),這時候使用完成處理程序是超級方便的秩命。接下來你會看到蘋果的API尉共,如dataTaskWithRequest
中的完成處理程序,及后面我們在使用我們自己API時的完成處理程序弃锐。
dataTaskWithRequest
中的完成處理程序的方法簽名如下:
completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void
這是一段代碼塊(->將告訴我們返回值類型袄友,如果有的話)。它有三個參數(shù):(NSData?, NSURLResponse?, NSError?)
及返回類型:Void
霹菊。對于一個具體的完成處理程序的內(nèi)聯(lián)代碼如下:
let task = session.dataTaskWithRequest(urlRequest, completionHandler:
{ (data, response, error) in
// 這里是完成處理程序的具體代碼
})
task.resumt()
注意剧蚣,花括號中間的那段代碼,有三個參數(shù)(data, response, error)
旋廷,與完成處理程序中聲明的參數(shù)(NSData?, NSURLResponse?, NSError?)
是對應(yīng)的鸠按。你可以在代碼段顯示的指出,也可以省略柳洋,這個不是必須的待诅,因為編譯器可以推導(dǎo)出來。所要記住的是熊镣,代碼不僅僅是給電腦執(zhí)行的卑雁,更多的時候是給我們自己看,因此绪囱,不要弄得太晦澀测蹲。
let task = session.dataTaskWithRequest(urlRequest,
completionHandler:{(data: NSData?, response: NSURLResponse?,
error: NSError?) in
// 這里是完成處理程序的具體代碼
print(response)
print(error)
})
task.resume()
有點混亂是嗎!實際上你完全可以刪除completionHandler
聲明鬼吵,直接將代碼放在函數(shù)調(diào)用的后面扣甲。這完全和上面的代碼是一樣的,這種寫法在Swift
是非常常見的(譯者注:這種在Swift中稱為尾隨閉包):
let task = session.dataTaskWithRequest(urlRequest) { (data, response, error) in
// 完成處理程序?qū)⒎旁谶@里
print(response)
print(error)
}
task.resume()
如果將參數(shù)名稱設(shè)置為_
就是告訴編譯器,你會忽略掉該參數(shù):
let task = session.dataTaskWithRequest(urlRequest) { (data, _, error) in
// 這時候?qū)⒉荒軌虼蛴esponse琉挖,因為我們忽略了該參數(shù)
print(error)
}
task.resume()
我們也可以將完成處理函數(shù)聲明為一個變量启泣,然后把這個變量傳給dataTaskWithRequest
。這樣示辈,當(dāng)我們需要在多個地方調(diào)用時會非常方便寥茫。在后面實現(xiàn)OAuth2.0
認(rèn)證流程時我們會使用這種方式,因為還有很多要處理矾麻,所以現(xiàn)在我們會在調(diào)用失敗處理的時候使用纱耻。
下面的代碼就是完成處理程序的變量方式:
let myCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> Void = {
(data, response, error) in
// 完成處理代碼
print(response)
print(error)
}
let task = session.dataTaskWithRequest(urlRequest,
completionHandler: myCompletionHandler)
task.resume()
那么上面這段代碼發(fā)生了什么呢?嗯险耀,當(dāng)我們調(diào)用dataTaskWithRequest
函數(shù)后弄喘,它不會馬上被執(zhí)行。但甩牺,蘋果在實現(xiàn)dataTaskWithRequest
會像下面調(diào)用:
public func dataTaskWithRequest(request: NSURLRequest,
completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void)
-> NSURLSessionDataTask {
// 進(jìn)行URL請求
// 等待請求結(jié)果
// 檢查返回是否有錯誤蘑志,并進(jìn)行處理
completionHandler(data, response, error)
// 返回 data task
}
當(dāng)然,在你的代碼中不用像上面那樣去寫柴灯,因為dataTaskWithRequest
中已經(jīng)實現(xiàn)卖漫。事實上,可能會有幾個類似上面的調(diào)用來處理成功或者失敗赠群。而完成處理程序僅僅就是待在那里,等待dataTaskWithRequst
處理完畢后調(diào)用旱幼。
那么什么是完成處理程序呢查描?簡單來說,就是我們可以用來在一個動作完成后再進(jìn)行某些處理柏卤。比如冬三,像上面我們用來打印調(diào)用的結(jié)果,及可能出現(xiàn)的錯誤缘缚,以便驗證API調(diào)用是否正常工作勾笆。讓我們回到我們例子中,在完成處理程序?qū)懸稽c有用的代碼桥滨。然后窝爪,代碼將變成:
let task = session.dataTaskWithRequest(urlRequest, completionHandler:
{ (data, response, error) in
// 做一些有意義的事
})
task.resume()
現(xiàn)在我們可以訪問3個對象:請求的響應(yīng),請求的返回數(shù)據(jù)及請求的錯誤(如果有的話)齐媒。下面我們首先進(jìn)行錯誤檢查蒲每,然后給出如何得到我們想要的數(shù)據(jù):第一篇帖子的標(biāo)題。接下來我們將:
- 確定我們得到了數(shù)據(jù)喻括,并且沒有錯誤邀杏;
- 嘗試將數(shù)據(jù)轉(zhuǎn)換為JSON格式(因為,API返回的數(shù)據(jù)格式是JSON)唬血;
- 獲取帖子的標(biāo)題并打印望蜡。
注意唤崭,這里你需要在代碼中增加
import Foundation
,以便可以使用NSJSONSerialization
脖律。
let task = session.dataTaskWithRequest(urlRequest, completionHandler): {
(data, response, error) in
guard let responseData = data else {
print("錯誤: 沒有接收到返回數(shù)據(jù)")
return
}
guard error == nil else {
print("獲取/posts/1時有錯誤")
print(error)
return
}
// 將返回的數(shù)據(jù)解析為JSON格式
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options:[]) as! NSDictionary
} catch {
print("將數(shù)據(jù)轉(zhuǎn)換為JSON時出錯")
return
}
// 現(xiàn)在我們可以訪問post
print("帖子:" + post.description)
// 因為post是一個dictionary(字典)浩姥,因此我們可以通過"title"來獲取帖子標(biāo)題
if let postTitle = post["title"] as? String {
print("帖子標(biāo)題: " + postTitle)
}
})
task.resume()
運行后,輸出如下:
帖子: {
body = "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderi\
t molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto";
id = 1;
title = "sunt aut facere repellat provident occaecati excepturi optio reprehenderit";
userId = 1;
}
帖子標(biāo)題: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
這個有點冗長状您,假如你僅僅需要快速發(fā)起一個GET
請求勒叠,并且不需要身份驗證,那么就是這樣膏孟。
但如果請求的方法類型不是GET
眯分,那么你需要使用一個可變的NSURLRequest
,通過NSURLRequest
就可以設(shè)置請求的方法了:
let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts"
let postUrlRequest = NSMutableURLRequest(URL: NSURL(string: postEndpoint)!)
postUrlRequest.HTTPMethod = "POST"
這樣我們就是將新建的帖子內(nèi)容設(shè)置給請求的HTTPBody
中:
let newPost: NSDictionary = ["title": "Frist Post", "body": "I is fisrt",
"userId":1]
do {
let jsonPost = try NSJSONSerialization.dataWithJSONObject(newPost, options:[])
postUrlRequest.HTTPBody = jsonPost
} catch {
print("錯誤:無法創(chuàng)建帖子的JSON對象")
}
現(xiàn)在我們就可以執(zhí)行請求了(假設(shè)我們現(xiàn)在還持有前面創(chuàng)建的會話信息):
let task = session.dataTaskWithRequest(postUrlRequest, completionHandler: nil)
task.resume()
如果上面的代碼正常工作柒桑,那么我們在發(fā)送后將會得到新帖子的ID
弊决。因為這只是用于測試,JSONPlaceholder會讓你做各種REST請求(GET魁淳,POST飘诗,PUT,PATCH界逛,DELETE及OPTIONS)昆稿,但不會真正改變數(shù)據(jù)。所以息拜,當(dāng)我們新建了帖子后溉潭,我們會得到新帖子的ID,以確認(rèn)我們程序工作正常少欺,但它實際上不會保存到數(shù)據(jù)庫中喳瓣,所以我們是不能訪問它的。
let newPost: NSDictionary = ["title": "Frist Post",
"body": "I is fisrt", "userId":1]
do {
let jsonPost = try NSJSONSerialization.dataWithJSONObject(newPost,
options:[])
postUrlRequest.HTTPBody = jsonPost
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(postsUrlRequest,
completionHandler: { (data, response, error) in
guard let responseData = data else {
print("錯誤:沒有接收到數(shù)據(jù)")
return
}
guard error == nil else {
print("調(diào)用POST /posts 時出現(xiàn)錯誤")
print(error)
return
}
// 解析成JSON
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options:[]) as! NSDictionary
} catch {
print("解析POST /posts的返回時出錯")
return
}
// 打印帖子內(nèi)容
print("帖子:" + post.description)
// 打印帖子的ID
if let postID = post["id"] as? Int {
print("帖子ID:\(postID)")
}
})
task.resume()
}
刪除帖子的代碼非常類似(不需要帖子創(chuàng)建后的代碼):
let firstPostEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
let firstPostUrlRequest = NSMutableRequest(URL: NSURL(string: firstPostEndpoint)!)
firstPostUrlRequest.HTTPMethod = "DELETE"
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(firstPostUrlRequest,
completionHandler: { (data, response, error) in
guard let _ = data else {
print("調(diào)用DELEE /posts/1 時出現(xiàn)錯誤")
return
}
})
task.resume()
這是使用Swift調(diào)用REST API快速的赞别、骯臟的方式畏陕。這里會有幾個陷阱:因為我們假設(shè)了調(diào)用會成功得到返回數(shù)據(jù),并且數(shù)據(jù)的格式是我們所期望的仿滔。如果這會引起一個問題惠毁,一旦返回值的類型不是字典類型,那么整個App就會崩潰堤撵。
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options:[] as! NSDictionary)
當(dāng)然仁讨,我們可以通過檢查返回的數(shù)據(jù)確保不為空并且是一個字典類型(但,這會讓冗長變的很快)实昨,或者我們使用SwiftyJSON來代替:
// 解析成JSON
let post = JSON(data: responseData)
if let postID = post["id"].int {
print("帖子ID:\(postID)")
}
SwiftyJSON在每一步都會進(jìn)行檢查洞豁,如果post
為空,或者post["id"]
為空,則postID
也會為空丈挟。這時候.int
將返回一個可選的刁卜,就像Int?
。如果你確保值不會為空曙咽,那么可以使用.intValue
蛔趴,而不是得到一個可選值。
SwiftyJSON不僅僅可以處理整數(shù)例朱。下面列出了如何處理string
孝情、double
及boolean
:
let title = myJSON["title"].string
let cost = myJSON["cost"].double
let isPurchased = myJSON["purchased"].bool
如果JSON對象是一個數(shù)組(比如:帖子列表),那么你可以通過數(shù)組索引來訪問響應(yīng)的屬性洒嗤,如:
let thirdPostTitle = posts[3]["title"].string
到目前為止箫荡,這些代碼非常冗長,而且也沒有什么抽象渔隶。你本來是在處理帖子的功能羔挡,卻不得不寫一大堆代碼來處理網(wǎng)絡(luò)請求及數(shù)據(jù)處理〖浒Γ看來Alamofire將是我們一個比較好的選擇:
Alamofire.request(.GET, postEndpoint).responseJSON { response in
// 獲取錯誤
print(response.result.error)
// 獲取序列化后的數(shù)據(jù)(如JSON)
print(response.result.value)
// 獲取原始數(shù)據(jù)
print(response.data)
// 獲取NSHTTPURLResponse
print(response.response)
}
獲取GitHub上的代碼:REST gists