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

来源:互联网 发布:淘宝发货默认快递公司 编辑:程序博客网 时间:2024/05/23 18:34

重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习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上的代码。

原创粉丝点击