重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习Swift,看到这个教程对开发一个实际的APP非常有帮助,所以翻译共享给大家。原教程非常长,我会陆续翻译并发布,欢迎交流与分享。
我已准备好到直接跳到了一些对你有用的代码。但是,苹果在iOS9中引入App Transport Security,虽然ATS是保护从你iPhone中发送和接受的数据,但对于开发人员来说的确有点头痛。
ATS要求使用SSL进行数据传输,这对实施来说是非常挑剔的。可悲的是,现在很多服务器是不满足这项要求的。所以,如果我们需要与访问这些服务器时该怎么解决呢?接下来我们会解决这个问题,因为后面讲解网络调用基础知识所使用的服务器是需要先解决这个问题的。
我们将为这种应用情况下的应用程序传输安全增加一个异常。虽然我们可以禁用ATS,但这样比只为一台服务器创建异常更安全。在本章我们所要访问的API是http://jsonplaceholder.typicode.com/
。
我们需要在项目的info.plist
文件中增加一些键,以便创建该异常。我们会添加一个NSAppTransportSecurity
的字典,该字典中包含NSExceptionDomains
字典,存放服务器:jsonplaceholder.typicode.com
(注意:没有结尾的斜杠,也没有http或https前缀)。在jsonplaceholder.typicode.com
字典中增加一个布尔值的条目NSThirdPartyExceptionAllowsInsecureHTTPLoads
,并把值设置为YES
:
这样,下面我们就可以正式开始编写代码了。
使用Swift进行简单的REST API调用
当今几乎每一个APP都会通过API获取或创建内容。在本书中,我们会使用一个非常强大的网络处理库Alamofire,当然你也可以使用NSURLSession
快速的,不是那么优雅的执行异步数据请求任务。
在Swift进行异步URL请求的方法如下:
public func dataTaskWithRequest(request: NSURLRequest,
completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void)
-> NSURLSessionDataTask
该方法将向特定的URL发起一个请求,请求发送完毕后将停止运行。一旦得到对方服务器的响应(甚至是一个错误报告),相应的完成处理程序(completion handler)将被调用。我们可以在完成处理程序中对调用的结果进行处理,包括错误检查、将数据保存到本地、更新界面,等等。在实现dataTaskWithRequest
章节我们会深入讨论完成处理程序。
最简单的例子就是一个GET
请求。不过,我们需要一个API来进行测试,幸运的是JSONPlaceholder可以充当这个工具。
JSONPlaceholder是一个用来测试和制作原型的在线伪REST API。就像网页中的图像占位符。
JSONPlaceholder中有一些资源可以使用,类似于我们在很多应用中使用的:用户、帖子、照片、相册...等等。不过我们这里使用帖子(posts)。
首先,让我们先打印出第一个帖子的标题。我们可以通过发起一个到posts
端点(endpoint)的GET
请求,参数为帖子的ID
,来获取一个帖子的详细信息。从http://jsonplaceholder.typicode.com/posts/我们可以知道第一个帖子的ID
是1。下面就让我们获取它:
首先,我们设置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
关键字可以让我们检查所请求的URL是否有效。
接下来,使用NSURLSession
发送请求:
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
然后,创建数据任务(data task):
let task = session.dataTaskWithRequest(urlRequest, completionHandler: nil)
最后,发送请求(是的,这个方法名称有点怪异)
task.resume()
现在我们将调起URL(通过urlRequest),并获得相应的结果(作为默认将使用GET
请求)。因此我们必须实现一个完成处理程序对返回结果进行处理,以便完成相应的处理。
当你第一次运行时,完成处理程序可能会有点混乱。一方面,它是一个变量或参数;另一方面,它又是一堆代码。如果你以前没有用过这种程序(块或者闭包),你会觉得非常奇怪。
当你的应用需要花费一些时间进行任务处理,比如:API调用,并且在任务处理完毕后需要执行某些动作处理,如更新界面显示新的数据,这时候使用完成处理程序是超级方便的。接下来你会看到苹果的API,如dataTaskWithRequest
中的完成处理程序,及后面我们在使用我们自己API时的完成处理程序。
dataTaskWithRequest
中的完成处理程序的方法签名如下:
completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void
这是一段代码块(->将告诉我们返回值类型,如果有的话)。它有三个参数:(NSData?, NSURLResponse?, NSError?)
及返回类型:Void
。对于一个具体的完成处理程序的内联代码如下:
let task = session.dataTaskWithRequest(urlRequest, completionHandler:
{ (data, response, error) in
// 这里是完成处理程序的具体代码
})
task.resumt()
注意,花括号中间的那段代码,有三个参数(data, response, error)
,与完成处理程序中声明的参数(NSData?, NSURLResponse?, NSError?)
是对应的。你可以在代码段显示的指出,也可以省略,这个不是必须的,因为编译器可以推导出来。所要记住的是,代码不仅仅是给电脑执行的,更多的时候是给我们自己看,因此,不要弄得太晦涩。
let task = session.dataTaskWithRequest(urlRequest,
completionHandler:{(data: NSData?, response: NSURLResponse?,
error: NSError?) in
// 这里是完成处理程序的具体代码
print(response)
print(error)
})
task.resume()
有点混乱是吗!实际上你完全可以删除completionHandler
声明,直接将代码放在函数调用的后面。这完全和上面的代码是一样的,这种写法在Swift
是非常常见的(译者注:这种在Swift中称为尾随闭包):
let task = session.dataTaskWithRequest(urlRequest) { (data, response, error) in
// 完成处理程序将放在这里
print(response)
print(error)
}
task.resume()
如果将参数名称设置为_
就是告诉编译器,你会忽略掉该参数:
let task = session.dataTaskWithRequest(urlRequest) { (data, _, error) in
// 这时候将不能够打印response,因为我们忽略了该参数
print(error)
}
task.resume()
我们也可以将完成处理函数声明为一个变量,然后把这个变量传给dataTaskWithRequest
。这样,当我们需要在多个地方调用时会非常方便。在后面实现OAuth2.0
认证流程时我们会使用这种方式,因为还有很多要处理,所以现在我们会在调用失败处理的时候使用。
下面的代码就是完成处理程序的变量方式:
let myCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> Void = {
(data, response, error) in
// 完成处理代码
print(response)
print(error)
}
let task = session.dataTaskWithRequest(urlRequest,
completionHandler: myCompletionHandler)
task.resume()
那么上面这段代码发生了什么呢?嗯,当我们调用dataTaskWithRequest
函数后,它不会马上被执行。但,苹果在实现dataTaskWithRequest
会像下面调用:
public func dataTaskWithRequest(request: NSURLRequest,
completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void)
-> NSURLSessionDataTask {
// 进行URL请求
// 等待请求结果
// 检查返回是否有错误,并进行处理
completionHandler(data, response, error)
// 返回 data task
}
当然,在你的代码中不用像上面那样去写,因为dataTaskWithRequest
中已经实现。事实上,可能会有几个类似上面的调用来处理成功或者失败。而完成处理程序仅仅就是待在那里,等待dataTaskWithRequst
处理完毕后调用。
那么什么是完成处理程序呢?简单来说,就是我们可以用来在一个动作完成后再进行某些处理。比如,像上面我们用来打印调用的结果,及可能出现的错误,以便验证API调用是否正常工作。让我们回到我们例子中,在完成处理程序写一点有用的代码。然后,代码将变成:
let task = session.dataTaskWithRequest(urlRequest, completionHandler:
{ (data, response, error) in
// 做一些有意义的事
})
task.resume()
现在我们可以访问3个对象:请求的响应,请求的返回数据及请求的错误(如果有的话)。下面我们首先进行错误检查,然后给出如何得到我们想要的数据:第一篇帖子的标题。接下来我们将:
- 确定我们得到了数据,并且没有错误;
- 尝试将数据转换为JSON格式(因为,API返回的数据格式是JSON);
- 获取帖子的标题并打印。
注意,这里你需要在代码中增加
import Foundation
,以便可以使用NSJSONSerialization
。
let task = session.dataTaskWithRequest(urlRequest, completionHandler): {
(data, response, error) in
guard let responseData = data else {
print("错误: 没有接收到返回数据")
return
}
guard error == nil else {
print("获取/posts/1时有错误")
print(error)
return
}
// 将返回的数据解析为JSON格式
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options:[]) as! NSDictionary
} catch {
print("将数据转换为JSON时出错")
return
}
// 现在我们可以访问post
print("帖子:" + post.description)
// 因为post是一个dictionary(字典),因此我们可以通过"title"来获取帖子标题
if let postTitle = post["title"] as? String {
print("帖子标题: " + 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;
}
帖子标题: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
这个有点冗长,假如你仅仅需要快速发起一个GET
请求,并且不需要身份验证,那么就是这样。
但如果请求的方法类型不是GET
,那么你需要使用一个可变的NSURLRequest
,通过NSURLRequest
就可以设置请求的方法了:
let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts"
let postUrlRequest = NSMutableURLRequest(URL: NSURL(string: postEndpoint)!)
postUrlRequest.HTTPMethod = "POST"
这样我们就是将新建的帖子内容设置给请求的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("错误:无法创建帖子的JSON对象")
}
现在我们就可以执行请求了(假设我们现在还持有前面创建的会话信息):
let task = session.dataTaskWithRequest(postUrlRequest, completionHandler: nil)
task.resume()
如果上面的代码正常工作,那么我们在发送后将会得到新帖子的ID
。因为这只是用于测试,JSONPlaceholder会让你做各种REST请求(GET,POST,PUT,PATCH,DELETE及OPTIONS),但不会真正改变数据。所以,当我们新建了帖子后,我们会得到新帖子的ID,以确认我们程序工作正常,但它实际上不会保存到数据库中,所以我们是不能访问它的。
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("错误:没有接收到数据")
return
}
guard error == nil else {
print("调用POST /posts 时出现错误")
print(error)
return
}
// 解析成JSON
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options:[]) as! NSDictionary
} catch {
print("解析POST /posts的返回时出错")
return
}
// 打印帖子内容
print("帖子:" + post.description)
// 打印帖子的ID
if let postID = post["id"] as? Int {
print("帖子ID:\(postID)")
}
})
task.resume()
}
删除帖子的代码非常类似(不需要帖子创建后的代码):
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("调用DELEE /posts/1 时出现错误")
return
}
})
task.resume()
这是使用Swift调用REST API快速的、肮脏的方式。这里会有几个陷阱:因为我们假设了调用会成功得到返回数据,并且数据的格式是我们所期望的。如果这会引起一个问题,一旦返回值的类型不是字典类型,那么整个App就会崩溃。
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options:[] as! NSDictionary)
当然,我们可以通过检查返回的数据确保不为空并且是一个字典类型(但,这会让冗长变的很快),或者我们使用SwiftyJSON来代替:
// 解析成JSON
let post = JSON(data: responseData)
if let postID = post["id"].int {
print("帖子ID:\(postID)")
}
SwiftyJSON在每一步都会进行检查,如果post
为空,或者post["id"]
为空,则postID
也会为空。这时候.int
将返回一个可选的,就像Int?
。如果你确保值不会为空,那么可以使用.intValue
,而不是得到一个可选值。
SwiftyJSON不仅仅可以处理整数。下面列出了如何处理string
、double
及boolean
:
let title = myJSON["title"].string
let cost = myJSON["cost"].double
let isPurchased = myJSON["purchased"].bool
如果JSON对象是一个数组(比如:帖子列表),那么你可以通过数组索引来访问响应的属性,如:
let thirdPostTitle = posts[3]["title"].string
到目前为止,这些代码非常冗长,而且也没有什么抽象。你本来是在处理帖子的功能,却不得不写一大堆代码来处理网络请求及数据处理。看来Alamofire将是我们一个比较好的选择:
Alamofire.request(.GET, postEndpoint).responseJSON { response in
// 获取错误
print(response.result.error)
// 获取序列化后的数据(如JSON)
print(response.result.value)
// 获取原始数据
print(response.data)
// 获取NSHTTPURLResponse
print(response.response)
}
获取GitHub上的代码:REST gists