(Swift) iOS Apps with REST APIs(
重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习Swift,看到这个教程对开发一个实际的APP非常有帮助,所以翻译共享给大家。原教程非常长,我会陆续翻译并发布,欢迎交流与分享。
使用Alamofire和SwiftyJSON进行REST API调用
上面我们使用了快速、肮脏的方式在iOS中访问REST API。dataTaskWithRequest
对于比较简单的情况还是非常不错的。但是如今大量的应用都需要使用web服务,并在寻找一种具有更好的处理方式,能够在更高层面上的抽象,具有更简洁的语法,更简单的数据处理,暂停/恢复及进度指示等...
在Objective-C
中有AFNetworking,在Swift
中我们可以使用Alamofire。
虽然我们一直在强调要优雅,但JSON的解析是相当的难看啊。可选的绑定(Swift1.2)虽然可以帮助我们,但SwiftyJSON才能够真正的帮到我们。
和前面一样,下面我们继续使用JSONPlaceholder作为要调用的API。
这是我们前面所写的代码,快但不优雅。(注意你要在头部添加import Foundation
才能够使用NSJSONSerialization
)。
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)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
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
}
// parse the result as JSON, since that's what the API provides
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()
对于我们要做的事这代码多的可怕(当然,相对于由WSDL Web服务生成千上万行的代码,Xcode只是滚动这些文件就会崩溃的黑暗时代,这些代码还是非常少的)。而且还没有认证,错误检查也是刚刚够用。
那么接下来让我们看看如果使用我不停提到的Alamofire库会怎样。首先你需要使用CocoaPods(如果你没有了解过,可以先参看后面的CocoaPods简介)将Alamofire v3.1添加到项目中。(译者注:这里你可以参考这篇博文了解CocoaPods,《用CocoaPods做iOS程序的依赖管理》)。接下来设置请求:
let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
Alamofire.request(.GET, postEndpoint)
.responseJSON { response in
// ...
}
看,这对我们来说可读性是不是增强了。我们使用Alamofire设置并发起一个异步请求到postEndpoint
(这里没有丑陋的USURL
相关代码)。我们明确的使用GET
请求(而不是NSURLRequest
的假定)。.GET
是Alamofire.Method
枚举对象的成员,该对象还有:.POST
、.PATCH
、.OPTIONS
、.DELETE
等。
接下来,在.responseJSON
方法中获得异步返回的JSON数据。我们也可以使用.response
(对于NSHTTPURLResponse
对象),.responsePropertyList
,或者.responseString
(对于字符串对象)。我们甚至在调试的时候链式调用多个.responseX
方法:
Alamofire.request(.GET, postEndpoint)
.responseJSON { response in
// 处理JSON
}
.responseString{ response in
// 在调试、测试的时候把返回数据以字符串的方式打印出来
print(response.result.value)
// 检查错误
print(response.result.error)
}
非常棒对么!但现在我们只想从JSON对象中获取帖子的标题。我们将发起一个请求并使用.responseJSON
对返回进行处理。和上面一样也会对错误进行检查和处理:
- 对API调用进行错误检查;
- 如果没有错误,那么看看我们是否获得了JSON数据;
- 检查JSON转换时是否有错误;
- 如果没有错误,那么从JSON对象中获取并打印帖子标题。
在SwiftyJSON中,
post["title"] as? String
可以替换成更简洁的方式:
post["title"].string
当我们仅仅是对一个层级对象进行解包时改变不是很大,但对于多个级别的嵌套数据解包时,如下面的代码:
if let postArray = data as? NSArray {
if let firstPost = postsArray[0] as? NSDictionary {
if let title = firstPost["title"] as ? String {
// ...
}
}
}
在Swift1.2中可以在一个if-let
语句中进行多个解包,但是也是非常难读的:
if let postsArray = data as? NSArray,
firstPost = postsArray[0] as? NSDictionary,
title = firstPost["title"] as? String {
// ...
}
对比一下SwiftyJSON的方式:
if let title = postsArray[0]["title"].string {
// ...
}
好了,一起的代码就是:
let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
Alamofire.request(.GET, postEndpoint)
.responseJSON { response in
guard response.result.error == nil else {
// 获取数据时出现错误
print("获取/posts/1时出现错误")
print(response.result.error!)
return
}
if let value: AnyObject = response.result.value {
// 将返回数据转换为JSON格式,不需要前面那么一大堆嵌套if-let
let post = JSON(value)
// 打印
// though a table view would definitely be better UI:
print("帖子: " + post.description)
if let title = post["title"].string {
// 访问字段
print("帖子标题: " + title)
} else {
print("解析出错")
}
}
}
我们检查Web服务的返回数据,并调用let post = JSON(value)
创建Post
对象,相对于之前的let post = NSJSONSerialization.JSONObjectWithData(data, options:[], error: &jsonError) as! NSDictionary
(如果不是dictionary
将崩溃)简单清晰多了。
对于POST
,我们更改HTTP
的请求方法,并提供所要提交的数据:
let postsEndpoint: String = "http://jsonplaceholder.typicode.com/posts"
let newPost = ["title": "Frist Psot", "body": "I iz fisrt", "userId": 1]
Alamofire.request(.POST, postsEndpoint, parameters: newPost, encoding: .JSON)
.responseJSON { response in
guard response.result.error == nil else {
// 出现错误
print("提交到/posts时出现错误")
print(response.result.error!)
return
}
if let value: AnyObject = response.result.value {
// 将返回值转换为JSON
let post = JSON(value)
print("帖子: " + post.description)
}
}
DELETE
(删除)将变的短小精悍:
let firstPostEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
Alamofire.request(.DELETE, firstPostEndpoint)
.responseJSON { response in
if let error = response.result.error {
// 出现错误
print("删除/posts/1出现错误")
print(error)
}
}
获取GitHub上的代码:REST gists
这对我们来说是迈向漂亮、干净的REST API调用旅程的一步。但我们仍然需要处理无类型JSON,这很容易导致错误。接下来,我们将为Post
对象定义一个类,并使用Alamofire自定义响应解析进行解析。
Alamofire路由(Router)
前面我们使用Alamofire和SwiftyJSON调用REST API。下面我们会继续使用Alamofire的路由(Router)继续进行简化,虽然对于这些简单的调用可能有点过。路由将组合URL请求,从而避免我们在代码中到处使用URL字符串。路由还可以用来设置请求的报头,比如:包含OAuth认证的令牌,或者其它认证的报头等信息。
使用Alamofire的路由是一种很好的做法,这样可以使我们的代码组织的更好。路由负责创建URL请求,从而简化我们的API管理器(或者API调用)。
前面的示例我们使用JSONPlaceholder服务,实现了get
、create
和delete
功能。JSONPlaceholder提供用来测试的和原型设计的在线REST API模拟服务,就像web开发时使用的图片占位符一样。
我们之前的代码如下:
// get
let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
Alamofire.request(.GET, postEndpoint)
.responseJSON { response in
guard response.result.error == nil else {
// 获取数据时出现错误
print("获取/posts/1时出现错误")
print(response.result.error!)
return
}
if let value: AnyObject = response.result.value {
// 将返回数据转换为JSON格式,不需要前面那么一大堆嵌套if-let
let post = JSON(value)
// 打印
// though a table view would definitely be better UI:
print("帖子: " + post.description)
if let title = post["title"].string {
// 访问字段
print("帖子标题: " + title)
} else {
print("解析出错")
}
}
}
// create
let postsEndpoint: String = "http://jsonplaceholder.typicode.com/posts"
let newPost = ["title": "Frist Psot", "body": "I iz fisrt", "userId": 1]
Alamofire.request(.POST, postsEndpoint, parameters: newPost, encoding: .JSON)
.responseJSON { response in
guard response.result.error == nil else {
// 出现错误
print("提交到/posts时出现错误")
print(response.result.error!)
return
}
if let value: AnyObject = response.result.value {
// 将返回值转换为JSON
let post = JSON(value)
print("帖子: " + post.description)
}
}
// delete
let firstPostEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
Alamofire.request(.DELETE, firstPostEndpoint)
.responseJSON { response in
if let error = response.result.error {
// 出现错误
print("删除/posts/1出现错误")
print(error)
}
}
下面我们将要修改Alamofire.request(...)
部分。现在,我们在程序中使用URL字符串,如:http://jsonplaceholder.typicode.com/posts/1
,及HTTP方法,如:.GET
。取代这两个参数的是Alamofire.request(...)
中的URLRequestConvertible
,如:NSMutableURLRequest
对象。我们下面通过路由来进一步提升。
使用Alamofire路由
让我们开始构建路由。所要构建的路由是一个枚举类型,包含了我们所需要的每一种调用。在Swift中,一个高级特性就是枚举的case
可以使用参数。举个例子来熟,就是我们的.Get
方法可以有一个Int
参数,这样我们就可以通过设置改参数来获取指定的帖子。
我们调用API时还需要一个基础URL路径。这样我们就可以使用计算属性来获得NSMutableURLRequest
,这个又是Swift枚举类型的另一项非常棒的特性。
enum Router: URLRequestConvertible {
static let baseURLString = "http://jsonplaceholder.typicode.com/"
case Get(Int)
case Create([String: AnyObject])
case Delete(Int)
var URLRequest: NSMutableURLRequest {
...
// TODO: 待实现
}
}
我们后面再回来实现URLRequest的计算。这里,我们先看一下如何使用路由来修改之前的代码。
对于GET
的调用:
Alamofire.request(.GET, postEndpoint)
修改为:
Alamofire.request(Router.Get(1))
我们也可以删除下面这句,因为路由已经帮我们管理了:
let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
.POST
调用类似,将:
let postsEndpoint: String = "http://jsonplaceholder.typicode.com/posts"
let newPost = ["title": "Frist Psot", "body": "I iz fisrt", "userId": 1]
Alamofire.request(.POST, postsEndpoint, parameters: newPost, encoding: .JSON)
修改为:
let newPost = ["title": "Frist Psot", "body": "I iz fisrt", "userId": 1]
Alamofire.request(Router.Create(newPost))
你可以看到路由以抽象的方式很好的实现了此端点的功能。
.DELETE
调用将:
let firstPostEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
Alamofire.request(.DELETE, firstPostEndpoint)
更改为:
Alamofire.request(Router.Delete(1))
现在我们的调用是不是更加清晰可读了。我们还可以把路由的case
的命名更详细一些,如:Router.DeletePostWithID(1)
。
计算URL请求
本节的代码是相当Swift的,如果觉得有点怪怪的,先不要理,继续读下去。
在路由中我们需要实现一个计算属性,这样我们在调用,如:Router.Delete(1)
的时候就会得到一个NSMutableURLRequest,Alamofire.Request()
就知道该如何使用了。
Router
是一个枚举类型,包含了我们的3个调用。因此,我们需要为这3种情况来为URLRequest计算。如:我们可以使用switch
语句来为每一个调用定义Http method
:
同样,我们可以使用switch
来创建NSMutableURLRequest
:
var method: Alamofire.Method {
switch self {
case .Get:
return .GET
case .Create:
return .POST
case .Delete:
return .DELETE
}
}
在switch
语句中的参数(path: String, parameters:[String: AnyObject?]
)。将被使用的返回中,如.Create
中的返回("post", newPost)
。
你也可以为每一个case
设置参数,如在.Get
中.Get(let postNumber)
。
我们可以把这些弄在一起用来生成URL的请求。那么首先,我们来通过基础的URL计算请求的NSURL
:
let URL = NSURL(string: Router.baseURLString)!
然后添加switch
语句返回的子路径:
let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
再创建URL请求,并包含已经编码的参数(encoding.encode(...)
会处理nil
参数,所以这里我们不需要再进行检查):
let encoding = Alamofire.ParameterEncoding.JSON
let (encodeRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)
设置HTTP method
:
encodedRequest.HTTPMethod = method.rawValue
最后返回URL请求:
return encodedRequest
总起来就是:
var URLRequest: NSMutableURLRequest {
var method: Alamofire.Method {
switch self {
case .Get
return .GET
case .Create
return .POST
case .Delete
return .DELETE
}
}
let result: (path: String, parameters:[String: AnyObject]?) = {
switch self {
case .Get(let postNumber):
return ("posts/\(postNumber)", nil)
case .Create(let newPost):
return ("posts", newPost)
case .Delete(let postNumber)
return ("posts/\(postNumber)", nil)
}
}()
let URL = NSURL(string: Router.baseURLString)!
let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
let encoding = Alamofire.PatameterEncoding.JSON
let (encodedRequest, _) = encoding.encode(URLReqest, parameters: result.parameters)
encodedRequest.HTTPMethod = method.rawValue
return encodedRequest
}
保存并测试,在控制台日志中应该会打印出和前面一样的帖子标题,并且没有错误输出。到目前为止,我们已经创建了一个路由,并且可以用于你自己的API调用。
GitHub上的代码。