(Swift) iOS Apps with REST APIs(三) -- 使用Alamofire和SwiftyJSON进行REST API调用

重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习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的假定)。.GETAlamofire.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服务,实现了getcreatedelete功能。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上的代码。

你可能感兴趣的:((Swift) iOS Apps with REST APIs(三) -- 使用Alamofire和SwiftyJSON进行REST API调用)