(Swift) iOS Apps with REST APIs(二)

重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习Swift,看到这个教程对开发一个实际的APP非常有帮助,所以翻译共享给大家。原教程非常长,我会陆续翻译并发布,欢迎交流与分享。

我已准备好到直接跳到了一些对你有用的代码。但是,苹果在iOS9中引入App Transport Security,虽然ATS是保护从你iPhone中发送和接受的数据,但对于开发人员来说的确有点头痛。

ATS要求使用SSL进行数据传输,这对实施来说是非常挑剔的。可悲的是,现在很多服务器是不满足这项要求的。所以,如果我们需要与访问这些服务器时该怎么解决呢?接下来我们会解决这个问题,因为后面讲解网络调用基础知识所使用的服务器是需要先解决这个问题的。

我们将为这种应用情况下的应用程序传输安全增加一个异常。虽然我们可以禁用ATS,但这样比只为一台服务器创建异常更安全。在本章我们所要访问的API是http://jsonplaceholder.typicode.com/

我们需要在项目的info.plist文件中增加一些键,以便创建该异常。我们会添加一个NSAppTransportSecurity的字典,该字典中包含NSExceptionDomains字典,存放服务器:jsonplaceholder.typicode.com(注意:没有结尾的斜杠,也没有http或https前缀)。在jsonplaceholder.typicode.com字典中增加一个布尔值的条目NSThirdPartyExceptionAllowsInsecureHTTPLoads,并把值设置为YES
<div style="text-align: center">


</div>

这样,下面我们就可以正式开始编写代码了。

使用Swift进行简单的REST API调用

当今几乎每一个APP都会通过API获取或创建内容。在本书中,我们会使用一个非常强大的网络处理库Alamofire,当然你也可以使用NSURLSession快速的,不是那么优雅的执行异步数据请求任务。

Swift进行异步URL请求的方法如下:

  public func dataTaskWithRequest(request: NSURLRequest,    
    completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void)    
    -> NSURLSessionDataTask    

该方法将向特定的URL发起一个请求,请求发送完毕后将停止运行。一旦得到对方服务器的响应(甚至是一个错误报告),相应的完成处理程序(completion handler)将被调用。我们可以在完成处理程序中对调用的结果进行处理,包括错误检查、将数据保存到本地、更新界面,等等。在实现dataTaskWithRequest章节我们会深入讨论完成处理程序。

最简单的例子就是一个GET请求。不过,我们需要一个API来进行测试,幸运的是JSONPlaceholder可以充当这个工具。

JSONPlaceholder是一个用来测试和制作原型的在线伪REST API。就像网页中的图像占位符。

JSONPlaceholder中有一些资源可以使用,类似于我们在很多应用中使用的:用户、帖子、照片、相册...等等。不过我们这里使用帖子(posts)。

首先,让我们先打印出第一个帖子的标题。我们可以通过发起一个到posts端点(endpoint)的GET请求,参数为帖子的ID,来获取一个帖子的详细信息。从http://jsonplaceholder.typicode.com/posts/我们可以知道第一个帖子的ID是1。下面就让我们获取它:

首先,我们设置URL请求:

  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)

guard关键字可以让我们检查所请求的URL是否有效。

接下来,使用NSURLSession发送请求:

  let config = NSURLSessionConfiguration.defaultSessionConfiguration()
  let session = NSURLSession(configuration: config)

然后,创建数据任务(data task):

  let task = session.dataTaskWithRequest(urlRequest, completionHandler: nil)

最后,发送请求(是的,这个方法名称有点怪异)

  task.resume()

现在我们将调起URL(通过urlRequest),并获得相应的结果(作为默认将使用GET请求)。因此我们必须实现一个完成处理程序对返回结果进行处理,以便完成相应的处理。

当你第一次运行时,完成处理程序可能会有点混乱。一方面,它是一个变量或参数;另一方面,它又是一堆代码。如果你以前没有用过这种程序(块或者闭包),你会觉得非常奇怪。

当你的应用需要花费一些时间进行任务处理,比如:API调用,并且在任务处理完毕后需要执行某些动作处理,如更新界面显示新的数据,这时候使用完成处理程序是超级方便的。接下来你会看到苹果的API,如dataTaskWithRequest中的完成处理程序,及后面我们在使用我们自己API时的完成处理程序。

dataTaskWithRequest中的完成处理程序的方法签名如下:

  completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void

这是一段代码块(->将告诉我们返回值类型,如果有的话)。它有三个参数:(NSData?, NSURLResponse?, NSError?)及返回类型:Void。对于一个具体的完成处理程序的内联代码如下:

  let task = session.dataTaskWithRequest(urlRequest, completionHandler: 
  { (data, response, error) in
    // 这里是完成处理程序的具体代码
  })  
  task.resumt()

注意,花括号中间的那段代码,有三个参数(data, response, error),与完成处理程序中声明的参数(NSData?, NSURLResponse?, NSError?)是对应的。你可以在代码段显示的指出,也可以省略,这个不是必须的,因为编译器可以推导出来。所要记住的是,代码不仅仅是给电脑执行的,更多的时候是给我们自己看,因此,不要弄得太晦涩。

  let task = session.dataTaskWithRequest(urlRequest, 
    completionHandler:{(data: NSData?, response: NSURLResponse?, 
      error: NSError?) in 
    // 这里是完成处理程序的具体代码
    print(response)
    print(error)
  })
  task.resume()

有点混乱是吗!实际上你完全可以删除completionHandler声明,直接将代码放在函数调用的后面。这完全和上面的代码是一样的,这种写法在Swift是非常常见的(译者注:这种在Swift中称为尾随闭包):

  let task = session.dataTaskWithRequest(urlRequest) { (data, response, error) in 
    // 完成处理程序将放在这里
    print(response)
    print(error)
  }
  task.resume()

如果将参数名称设置为_就是告诉编译器,你会忽略掉该参数:

  let task = session.dataTaskWithRequest(urlRequest) { (data, _, error) in
    // 这时候将不能够打印response,因为我们忽略了该参数
    print(error)  
  }
  task.resume()

我们也可以将完成处理函数声明为一个变量,然后把这个变量传给dataTaskWithRequest。这样,当我们需要在多个地方调用时会非常方便。在后面实现OAuth2.0认证流程时我们会使用这种方式,因为还有很多要处理,所以现在我们会在调用失败处理的时候使用。

下面的代码就是完成处理程序的变量方式:

  let myCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> Void = {
    (data, response, error) in 
       // 完成处理代码
       print(response)
       print(error)
  }
  let task = session.dataTaskWithRequest(urlRequest, 
    completionHandler: myCompletionHandler)
  task.resume()

那么上面这段代码发生了什么呢?嗯,当我们调用dataTaskWithRequest函数后,它不会马上被执行。但,苹果在实现dataTaskWithRequest会像下面调用:

  public func dataTaskWithRequest(request: NSURLRequest,
    completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void)
    -> NSURLSessionDataTask {
    // 进行URL请求
    // 等待请求结果
    // 检查返回是否有错误,并进行处理
    completionHandler(data, response, error)
    // 返回 data task
 }

当然,在你的代码中不用像上面那样去写,因为dataTaskWithRequest中已经实现。事实上,可能会有几个类似上面的调用来处理成功或者失败。而完成处理程序仅仅就是待在那里,等待dataTaskWithRequst处理完毕后调用。

那么什么是完成处理程序呢?简单来说,就是我们可以用来在一个动作完成后再进行某些处理。比如,像上面我们用来打印调用的结果,及可能出现的错误,以便验证API调用是否正常工作。让我们回到我们例子中,在完成处理程序写一点有用的代码。然后,代码将变成:

  let task = session.dataTaskWithRequest(urlRequest, completionHandler:
    { (data, response, error) in
    // 做一些有意义的事
  })
  task.resume()

现在我们可以访问3个对象:请求的响应,请求的返回数据及请求的错误(如果有的话)。下面我们首先进行错误检查,然后给出如何得到我们想要的数据:第一篇帖子的标题。接下来我们将:

  • 确定我们得到了数据,并且没有错误;
  • 尝试将数据转换为JSON格式(因为,API返回的数据格式是JSON);
  • 获取帖子的标题并打印。

注意,这里你需要在代码中增加import Foundation,以便可以使用NSJSONSerialization

  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
    }
    
    // 将返回的数据解析为JSON格式
    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()

运行后,输出如下:

  帖子: {
    body = "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderi\
t molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"; 
    id = 1;
    title = "sunt aut facere repellat provident occaecati excepturi optio reprehenderit";
    userId = 1;
  }
  帖子标题: sunt aut facere repellat provident occaecati excepturi optio reprehenderit

这个有点冗长,假如你仅仅需要快速发起一个GET请求,并且不需要身份验证,那么就是这样。

但如果请求的方法类型不是GET,那么你需要使用一个可变的NSURLRequest,通过NSURLRequest就可以设置请求的方法了:

  let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts"
  let postUrlRequest = NSMutableURLRequest(URL: NSURL(string: postEndpoint)!)
  postUrlRequest.HTTPMethod = "POST"

这样我们就是将新建的帖子内容设置给请求的HTTPBody中:

  let newPost: NSDictionary = ["title": "Frist Post", "body": "I is fisrt", 
    "userId":1]
  do {
    let jsonPost = try NSJSONSerialization.dataWithJSONObject(newPost, options:[])
    postUrlRequest.HTTPBody = jsonPost
  } catch {
    print("错误:无法创建帖子的JSON对象")
  }

现在我们就可以执行请求了(假设我们现在还持有前面创建的会话信息):

  let task = session.dataTaskWithRequest(postUrlRequest, completionHandler: nil)
  task.resume()

如果上面的代码正常工作,那么我们在发送后将会得到新帖子的ID。因为这只是用于测试,JSONPlaceholder会让你做各种REST请求(GET,POST,PUT,PATCH,DELETE及OPTIONS),但不会真正改变数据。所以,当我们新建了帖子后,我们会得到新帖子的ID,以确认我们程序工作正常,但它实际上不会保存到数据库中,所以我们是不能访问它的。

  let newPost: NSDictionary = ["title": "Frist Post", 
    "body": "I is fisrt", "userId":1]
  do {
    let jsonPost = try NSJSONSerialization.dataWithJSONObject(newPost, 
      options:[])
    postUrlRequest.HTTPBody = jsonPost
    
    let config = NSURLSessionConfiguration.defaultSessionConfiguration()
    let session = NSURLSession(configuration: config)
    
    let task = session.dataTaskWithRequest(postsUrlRequest, 
      completionHandler: { (data, response, error) in
      guard let responseData = data else {
        print("错误:没有接收到数据")
        return
      }
      guard error == nil else {
        print("调用POST /posts 时出现错误")
        print(error)
        return
      }
      
      // 解析成JSON
      let post: NSDictionary
      do {
        post = try NSJSONSerialization.JSONObjectWithData(responseData,
          options:[]) as! NSDictionary
      } catch {
        print("解析POST /posts的返回时出错")
        return
      }
      
      // 打印帖子内容
      print("帖子:" + post.description)
    
      // 打印帖子的ID
      if let postID = post["id"] as? Int {
      print("帖子ID:\(postID)")
    }
  })
  task.resume()
}  

删除帖子的代码非常类似(不需要帖子创建后的代码):

  let firstPostEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
  let firstPostUrlRequest = NSMutableRequest(URL: NSURL(string: firstPostEndpoint)!)
  firstPostUrlRequest.HTTPMethod = "DELETE"
    
  let config = NSURLSessionConfiguration.defaultSessionConfiguration()
  let session = NSURLSession(configuration: config)
    
  let task = session.dataTaskWithRequest(firstPostUrlRequest,
    completionHandler: { (data, response, error) in
    guard let _ = data else {
      print("调用DELEE /posts/1 时出现错误")
      return
    }
  })
  task.resume()

这是使用Swift调用REST API快速的、肮脏的方式。这里会有几个陷阱:因为我们假设了调用会成功得到返回数据,并且数据的格式是我们所期望的。如果这会引起一个问题,一旦返回值的类型不是字典类型,那么整个App就会崩溃。

  post = try NSJSONSerialization.JSONObjectWithData(responseData, 
    options:[] as! NSDictionary)

当然,我们可以通过检查返回的数据确保不为空并且是一个字典类型(但,这会让冗长变的很快),或者我们使用SwiftyJSON来代替:

  // 解析成JSON
  let post = JSON(data: responseData)  
  if let postID = post["id"].int {
    print("帖子ID:\(postID)")
  }

SwiftyJSON在每一步都会进行检查,如果post为空,或者post["id"]为空,则postID也会为空。这时候.int将返回一个可选的,就像Int?。如果你确保值不会为空,那么可以使用.intValue,而不是得到一个可选值。

SwiftyJSON不仅仅可以处理整数。下面列出了如何处理stringdoubleboolean:

let title = myJSON["title"].string
let cost = myJSON["cost"].double
let isPurchased = myJSON["purchased"].bool

如果JSON对象是一个数组(比如:帖子列表),那么你可以通过数组索引来访问响应的属性,如:

let thirdPostTitle = posts[3]["title"].string

到目前为止,这些代码非常冗长,而且也没有什么抽象。你本来是在处理帖子的功能,却不得不写一大堆代码来处理网络请求及数据处理。看来Alamofire将是我们一个比较好的选择:

Alamofire.request(.GET, postEndpoint).responseJSON { response in
  // 获取错误
  print(response.result.error)
  // 获取序列化后的数据(如JSON)
  print(response.result.value)
  // 获取原始数据
  print(response.data)
  // 获取NSHTTPURLResponse
  print(response.response)
}

获取GitHub上的代码:REST gists

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

推荐阅读更多精彩内容