(Swift) iOS Apps with REST APIs(四) -- 自定义Alamofire的响应序列化

本文将继续前面的教程,讲解如何将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(...))则是在服务器中执行。另外,这里将PostID设置为空,那是因为该值是由服务器进行分配的。

接下来,我们对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>)的一大堆工作。

你可以把ResponseResult结构想象成一个包,这个包中是我们从请求的响应中获取数据,并把它序列化为我们所使用的格式。这就像我们去买东西。支付后,你得到几样东西:你购买的东西、找零和收据,或者错误的信息,如你的“卡被拒绝“,或”还差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中,我们创建了一个响应序列化处理器,并且与泛型TNSError一起工作。该序列化处理器使用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

下面我们来完成Postsave()方法:

  // 创建
  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的表单页面及下拉刷新和滑动删除。最后我们讨论一下离线时该如何进行处理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容