本文将继续前面的教程,讲解如何将REST API获取JSON格式的数据转换为Swift对象。
重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习Swift,看到这个教程对开发一个实际的APP非常有帮助,所以翻译共享给大家。原教程非常长,我会陆续翻译并发布,欢迎交流与分享。
在Alamofire的GET与POST调用中使用强类型
我们使用Alamofire进行REST请求。现在让我们清理一下,通过把JSON映射到强类型类,从而构建更高级别的抽象。这将使我们的代码组织性更好,也使得我们不需要一次记住太多代码的细节。
首先,我们需要构建一个类来处理Post
对象的类。创建的类为了方便进行调试,需要具有以下几个属性,一个Post
对象构造函数,一个描述方法可以打印出对象的所有属性:
class Post {
var title:String?
var body:String?
var id:Int?
var userId:Int?
required init?(aTitle: String?, aBody: String?, anId: Int?, aUserId: Int?) {
self.title = aTitle
self.body = aBody
self.id = anId
self.userId = aUserId
}
func description() -> String {
return "ID: \(self.id)" +
"User ID: \(self.userId)" +
"Title: \(self.title)\n" +
"Body: \(self.body)\n"
}
}
我们将使用路由器来创建URL请求。它可以装配请求信息,包括HTTP方法和URL,并在报头中附加相应的参数。在URL请求和JSON处理方面,我们不需要对路由做任何改变。它不需要知道Post
对象的任何信息。
接下来我们创建一个PostRouter.swift
文件,作为我们的路由器:
import Alamofire
enum PostRouter: URLRequestConvertible {
static let baseURLString = "http://jsonplaceholder.typicode.com/"
case Get(Int)
case Create([String: AnyObject])
case Delete(Int)
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: PostRouter.baseURLString)!
let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
let encoding = Alamofire.ParameterEncoding.JSON
let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)
encodedRequest.HTTPMethod = method.rawValue
return encodedRequest;
}
}
当进行API调用时,我喜欢进行逆向工作。开始调用的时候,我们喜欢先搞清楚它们时怎么样工作的。首先,我们希望能够通过ID
的值可以得到相应的帖子。
我们可以在视图控制器的viewWillAppear
方法中进行测试:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// MARK: 获取ID为1的Post
Post.postByID(1, completionHandler: { result in
if let error = result.error {
// 如果在调用中出现了错误,我们需要进行处理
print("调用/posts/1时出现错误")
print(error)
return
}
guard let post = result.value else {
print("调用/posts/1时出现错误:返回值为空")
return
}
// 调用成功
print(post.getDescription())
print(post.title)
})
}
postByID
有相应的完成处理程序。与前面所编写代码的不同是,我们不再用一个函数编写完成处理程序,而是在方法中直接进行处理。当我们实现postByID
时我们将看到它们是如何工作的,以及当函数怎么样来调用完成处理程序来处理返回的结果。
我们使用完成处理程序,可以让程序异步执行。这里你或许注意到整个程序中没有URL、没有请求,也没有JSON解析。它是由Post
来完成,而不是在抽象的底层来处理。
我们也希望能够新建一个Post
,并将它发送到服务器。这里我们将newPost.save
方法中使用尾随闭包来进行处理,所以我们可以在代码中删除completionHandler
标签:
// MARK: 保存
// 创建Post
guard let newPost =
Post(aTitle: "First Psot", aBody: "I iz first", anId: nil, aUserId: 1) else {
print("错误: newPost不是一个Post对象")
return
}
newPost.save { result in
if let error = result.error {
// 如果在调用中出现了错误,我们需要进行处理
print("调用 POST /posts时出现错误")
print(error)
return
}
guard let post = result.value else {
print("调用 POST /posts出现错误,返回值为空")
return
}
// 调用成功
print(post.description())
print(post.title)
}
我们这里将Post
的创建和保存分开,创建(Post(...)
)是在本地,而保存(newPost.save(...)
)则是在服务器中执行。另外,这里将Post
的ID
设置为空,那是因为该值是由服务器进行分配的。
接下来,我们对Alamofire进行设置,并看看Post
的调用是如何来完成的。首先我们看看GET
请求(这里我们已使用顺手的路由来创建URL请求):
Alamofire.request(PostRouter.Get(1))
.responseJSON { response in
// ...
}
使用.responseObject
来代替.responseJSON
会不会更好呢?Alamofire是允许我们自己定义响应序列化(response serializer)处理,这样我们就可以将API调用的结果转换成任何我们想要的。最直接的就是将返回的JSON序列化为一个对象。
响应序列化可以将URL请求返还的结果转换为我们需要型式。默认的,URL请求的返回是一个NSData,但我们愿意使用JSON或者一个对象进行相关处理。
要创建一个序列化对象,需要扩展Alamofire.Request
。我们将新建一个文件,名称为:AlamofireRequest+JSONSerializable.swift
。如下面所示,我们先将逐步了解它是如何运作的:
import Foundation
import Alamofire
import SwiftyJSON
extension Alamofire.Request {
public func responseObject<T: ResponseJSONObjectSerializable>(completionHandler:
Response<T, NSError> -> Void) -> Self {
let serializer = ResponseSerializer<T, NSError> { request, response, data, error in
guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "无法进行对象序列化,因为输入的数据为空。"
let error = Error.errorWithCode(.DataSerializationFailed, failureReason:
failureReason)
return .Failure(error)
}
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response,
responseData, error)
switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
if let object = T(json: json) {
return .Success(object)
} else {
let failureReason = "无法通过JSON创建对象。"
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason:
failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: serializer, completionHandler: completionHandler)
}
}
responseObject<...>(...)
作为Alamofire.Request
的新的响应处理器。它与标准的response
函数唯一的不同是,它使用了我们自定义的responseSerializer
对返回数据进行序列化处理。因此,我们可以像下面这样来调用:
Alamofire.request(PostRouter.Get(id))
.responseObject{ (response: Response<Post, NSError>) in
// Post相关处理
}
我们逐渐来了解responseObject
这个函数。首先,我们从函数的声明开始:
extension Alamofire.Request {
public func responseObject<T: ResponseJSONObjectSerializable>(completionHandler:
Response<T, NSError> -> Void) -> Self {
...
}
}
所定义的函数名称为:responseObject
。<T>
表示这是一个泛型方法,可以与不同类型的对象一起工作。<T: ResponseJSONObjectSerializable>
表示,对象的类型必须实现ResponseJSONObjectSeriablizable
协议(该协议是我们自己定义的)。定义这个协议,是为了能够让我们确定所传入对象的类型,必须实现一个参数是JSON
的构造方法(init function)。
responseObject
函数中使用唯一的参数为completionHandler
。正如你所想的,当我们解析JSON并创建了对象后(也就是说作为当前函数的完成处理程序)就会调用它。这样我们就可以异步进行处理,而调用者也不用等待,当我们处理完毕后就会通知它。
完成处理程序也是只有一个参数Response<T, NSErroe>
。这是Alamofire3中定义的响应结构,它帮助我们处理了一大堆的事情,把处理结果包装成Result
结构(里面是我们T
对象和/或错误),这样省去了我们过去处理(NSURLRequest?, NSHTTPURLResponse?, Result<T, NSError>)
的一大堆工作。
你可以把Response
和Result
结构想象成一个包,这个包中是我们从请求的响应中获取数据,并把它序列化为我们所使用的格式。这就像我们去买东西。支付后,你得到几样东西:你购买的东西、找零和收据,或者错误的信息,如你的“卡被拒绝“,或”还差8毛钱"等。所有这些就组合成了你购买时的响应。
你也可以想象为不论交易是否成功,都会有购买的物品和/或者错误信息。
Alamofire的这些结构是类似的。Result
包含了.Success
和.Failure
,这样呢可以判断是否有错误。Response
则封装了Result
、你的原始请求及返回的原始数据。
responseObject
返回了Alamofire.Request
对象。-> Self
则是声明返回的类型。
现在,我们来看一下`responseObject函数的结构:
public func responseObject<T: ResponseJSONObjectSerializable>(completionHandler:
Response<T, NSError> -> Void) -> Self {
let serializer = ResponseSerializer<T, NSError> { (request, response, data, error) in
// ...
}
return response(responseSerializer: serializer, completionHandler: completionHandler)
}
在responseObject
中,我们创建了一个响应序列化处理器,并且与泛型T
和NSError
一起工作。该序列化处理器使用URL请求的返回(request, response, data, error)
作为参数,使用Alamofire中定义的Result
类型返回成功(包含已解析对象)或者失败(包含错误信息)。responseObject
函数只是返回了我们刚刚创建的responseSerializer
,并把参数的中的完成处理程序也返回,这样就可以在需要的地方使用了。
现在,我们来看一下我们最终所实现的responseSerializer
:
let serializer = ResponseSerializer<T, NSError> { request, response, data, error in
guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "无法进行对象序列化,因为输入的数据为空。"
let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response,
responseData, error)
switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
if let object = T(json: json) {
return .Success(object)
} else {
let failureReason = "无法通过JSON创建对象。"
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason:
failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}
自定义响应序列化处理首先使用guard
进行数据有效性检查。然后使用SwiftyJSON将数据解析为JSON格式。接下来就是通过JSON数据来创建指定的对象:
let newObject = T(json: json)
如果不能通过JSON数据创建响应的对象,那么我们将返回相应的错误。
为了能够是序列化程序可以正常工作,我们需要定义一个ResponseJSONObjectSerializable
协议:
public protocol ResponseJSONObjectSerializable {
init?(json: SwiftyJSON.JSON)
}
定义该协议就是能够确保要转换的类必须实现了响应的构造函数。这也就告诉了泛型对象需要实现哪些处理。在现在这个例子中,类必须实现一个能够从JSON数据对象进行构造的方法。下面我们在Post
类中实现它:
final class Post: ResponseJSONObjectSerializable {
var title:String?
var body:String?
var id:Int?
var userId:Int?
...
required init?(json: SwiftyJSON.JSON) {
self.title = json["title"].string
self.body = json["body"].string
self.id = json["id"].int
self.userId = json["userId"].int
}
...
}
使用SwiftyJSON可以是非常方便把JSON中的内容解析为Post
对象的属性。
现在我们就可以在Post.postById()
中使用我们自定义的序列化了:
class Post {
...
// MARK: API调用
class func postByID(id: Int, completionHandler: (Result<Post, NSError>) -> Void) {
Alamofire.request(PostRouter.Get(id))
.responseObject { (response: Response<Post, NSError>) in
completionHandler(response.result)
}
}
}
这就是GET
请求中所使用的了。现在我们就可以调用非常棒的Post.postByID(1)
方法了。
但,是的,还有其它的需求。我们说过我们需要实现POST
请求,把新建Post
保存到服务上。
这种情况下,Alamofire中没有特别的限制要求,所以我们只需要保证可以把Post
数据正确的序列化为API调用的格式即可。
在Post
类中,我们只需要实现一个方法可以将Post
转换为键为字符串类型的Dictionary
即可(为了方便我们称为json
):
func toJSON() -> Dictionary<String, AnyObject> {
var json = Dictionary<String, AnyObject>()
if let title = title {
json["title"] = title
}
if let body = body {
json["body"] = body
}
if let id = id {
json["id"] = id
}
if let userId = userId {
json["userId"] = userId
}
return json
}
因为在Alamofire.Request
中使用Dictionary
作为参数类型,所以这里我们没有使用NSDictionary
。
下面我们来完成Post
的save()
方法:
// 创建
func save(completionHandler: (Result<Post, NSError>) -> Void) {
guard let fields:Dictionary<String, AnyObject> = self.toJSON() else {
print("error: error converting newPost fields to JSON")
return
}
Alamofire.request(PostRouter.Create(fields))
.responseObject { (response: Response<Post, NSError>) in
completionHandler(response.result)
}
}
小结
嗯,就是这样了!现在我们可以很漂亮的获取和保存Post
。更进一步的是,调用者也不需要知道Post
是如何获取和保存。这样我们也可以很方便的将Alamofire替换为RESTKit,或者另外一个完全不同的API调用框架,并且还不用对视图控制器做任何改变。
点击这里 获取本章代码。
现在不需要担心如何解析复杂的JSON数据。先从最简单的字符串、数字和布尔字段开始。后面我们会对复杂的数据进行解析,如数组、日期等。你可以从简单的一两个需要自定义序列化的API调用开始,或者使用本章的代码。
接下来我们将使用Alamofire来构建gists应用。我们构建我们所需要的API调用,并把结果显示到用户界面。用户界面包含一个表格视图,gists的详情页面,创建gists的表单页面及下拉刷新和滑动删除。最后我们讨论一下离线时该如何进行处理。