为什么使用像Alamofire这样的库 。整合REST API和表格视图

本文将继续前面的教程,继续讲解如何通过REST API获取数据列表并解析为Swift对象,然后显示在表格视图中。

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

为什么使用像Alamofire这样的库

关于编程中最难的两件事情有一堆的笑话。有人说最难的事情是命名、评估和off-by-one(译者注:off-by-one大小差一错误是程序设计中常见错误,具体可以参考这里off-by-one)错误。还有人说是评估和拿到回报。但我认为是固化需求,固化需求可以让你知道哪些东西需要做,并能够让你保持代码在同一级别上抽象。

那么什么是在同一级别上抽象呢?先让我们看看一些古老的,让人迷糊的Objective-C代码:

NSArray *myGists = [[NSArray alloc]] initWithObjects:
  [NSString stringWithString:@"text of gist 1"],
  [NSString stringWithString:@"text of gist 2"],
  nil];

// 使用myGists进行某些处理

[myGists release];

这段代码的核心功能就是想对Gists数组进行某些处理。但是对于程序员来说,这里想的不仅仅是gists,还要考虑内存的管理(如:分配alloc、释放release)。因此,对于他们来说,在脑中要同时处理2个不同层次的抽象。这些对象不仅是Gists对象,它们还是内存中的块。

当然,所有的Gists对象都是内存中的块。而且某个地方的代码也是这么去处理。但,它还是不应该与gists的业务操作,如收藏、编辑,在同一个地方。这也会把web service的调用混在一起了:

  • 一部分代码需要知道并处理底层的网络事务
  • 一部分代码要处理JSON
  • 还有一部分代码要处理gists(或你的对象)

这是三个层次的抽象,它们不需要(也不应该)混在一起。在同一层次的抽象上编码,要比在不同层次上来回切换理解代码要轻松的多。

你可以不需要像SwiftyJSON、Alamofire这样的库。但它们的确能把底层处理封装的更好。而且一旦它们开源,你还可以在需要的时候对代码进行调整修改,而你又会失去什么呢?

连接REST API和表格视图

UITableView控件是iOS应用中常用的控件。结合Web Service,就是很多App的核心业务功能,如:邮件、Twitter及Facebook,甚至苹果自己的备忘录,连App Store也是一样。

接下来我们将新建一个Xcode工程,通过GitHub的gists API获取数据。然后在表格视图中显示公共的gists列表。由此,我们将发起一个GET请求,并将返回的数据解析为JSON格式,然后让表格视图显示这些结果。

本章重点讲的是如何把API返回的数据绑定到表格视图中,不会涉及如何将UITableView控件添加到Swift应用这种基础知识。如果你对如何使用UITableView控件有困惑,请参考Apple's docs或者这个教程

如果你不想自己敲代码,请到GitHub下载本章的代码

1. 创建Swift工程

我们终于可以动手创建GitHub Gists应用了。首先我们需要在Xcode中创建一个工程:

启动Xcode。

创建一个master-detail类型Swift工程(Devices中你可以选择universal或者iPhone)。确保在创建工程时选择使用Swift语言,并且没有选中Core Data选项。

使用CoclaPods将Alamofire 3.1SwoftyJSON 2.3添加到工程中(如果不知道如何做,请参考这里)。

然后,打开类型为.xcworkspace文件。

由于我们现在还使用不到由Xcode生成的样板代码,先不要管它,后面我们涉及到的时候会来解释。这里你唯一需要注意的是,Xcode创建了两个视图控制器:一个表格视图控制器MasterViewController,和一个detailViewController详细页面视图控制器。而它们正好可以用来显示我们gists的列表和gist的详细信息。接下来几章我们都会与MasterViewController打交道。

创建一个新文件并命名为:GitHubAPIManager.swift。这个类将负责与API之间的处理,也可以称为API管理器。它可以帮我们把代码组织的更好,也避免使视图控制器的代码变成一个庞大的文件。同时,也方便我们可以在多个视图控制器之间共享代码。

在文件的头部,引入Alamofire和SwiftyJSON:

import Foundation
import Alamofire
import SwiftyJSON

class GitHubAPIManager {

}

如果你是使用的是其它API代码,那么最好这里将名称更改为合适的名称,而不是GitHubAPIManager

当你与API打交道的时候,通常我们会得到的是一堆代码,而不是一个对象。我们需要设置自定义报头,跟踪OAuth访问令牌,处理client secretsclient ID,处理认证或者其它常见的错误。为了将这些代码从App Delegate及我们的模型对象中分离,我们将会把它们统一到GitHubAPIManager中进行管理。

在本教程的实例中我们只与GitHub API打交道,所以这里只有一个API管理器。因此我们在该类中声明一个sharedInstance变量,这样其它调用者就可以通过它来获取GitHubAPIManager的唯一实例:

import Foundation
import Alamofire
import SwiftyJSON

class GitHubAPIManager {
  static let sharedInstance = GitHubAPIManager()
}

接下来我们就可以通过API请求获取不需认证的公共gists列表了。为了方便我们快速理解,这里当我们获取API请求结果后先在控制台打印出来。然后我们再把它和表格视图集成。

因此,我们先声明这个简单的方法:

class GitHubAPIManager {
  ...

  func printPublicGists() -> Void {
    // TODO: 待实现
  }
}

接下来让我们创建Router路由,并把新建的文件命名为:GistRouter.swift。该路由器将负责创建URL请求,从而能够让我们的API管理器保持简单。新建的路由器和前面类似,除了只有一个获取公共gists的GET调用:

import Foundation
import Alamofire

enum GistRouter: URLRequestConvertible {
  static let baseURLString:String = "https://api.github.com"
  case GetPublic() // GET https://api.github.com/gists/public
  var URLRequest: NSMutableURLRequest { 
    var method: Alamofire.Method {
      switch self { 
      case .GetPublic:
        return .GET
      }
    }

    let result: (path: String, parameters: [String: AnyObject]?) = { 
      switch self {
      case .GetPublic:
        return ("/gists/public", nil) 
      }
    }()

    let URL = NSURL(string: GistRouter.baseURLString)!
    let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path)) 

    let encoding = Alamofire.ParameterEncoding.JSON
    let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters) 

    encodedRequest.HTTPMethod = method.rawValue

    return encodedRequest  
  }
}

获取公共的gists:

func printPublicGists() -> Void { 
  Alamofire.request(GistRouter.GetPublic())
    .responseString { response in
      if let receivedString = response.result.value {
        print(receivedString)
      }
  }
}

为了可以测试该代码,你需要修改MasterViewControllerviewDidAppear方法。该方法在每次主视图显示的时候都会调用:

override func viewDidAppear(animated: Bool) { 
  super.viewDidAppear(animated)
  // 开始测试
  GitHubAPIManager.sharedInstance.printPublicGists() 
  // 结束测试
}

保存并运行。在模拟器或者你的手机上你将看到一个空的表格视图。但,如果API调用成功,你会在屏幕的底部(控制台)看到打印出的JSON数据:

"[{\\"url\\":\\"https://api.github.com/gists/35877917945abf44fc7a\\",\\"forks_url\\":\\"https://a\\ pi.github.com/gists/35877917945abf44fc7a/forks\\",\\"commits_url\\":\\"https://api.github.com/\\ gists/35877917945abf44fc7a/commits\\",\\"id\\":\\"35877917945abf44fc7a\\",\\"git_pull_url\\":\\"ht\\ tps://gist.github.com/35877917945abf44fc7a.git\\",\\"git_push_url\\":\\"https://gist.github.co\\ m/35877917945abf44fc7a.git\\",\\"html_url\\":\\ ...

在你的API管理器中添加一个与printPublicGists类似的方法。它将获取到一个对象数组,并在控制台中打印。

2. 解析API返回的JSON数据

API调用返回了一个包含gists数组的JSON对象。在API docs for gists中描述了JSON对象的格式,包含了gists的作者信息、所包含的文件信息及历史版本信息等:

{
  "url": "https://api.github.com/gists/aa5a315d61ae9438b18d",
  "forks_url": "https://api.github.com/gists/aa5a315d61ae9438b18d/forks",
  "commits_url": "https://api.github.com/gists/aa5a315d61ae9438b18d/commits",
  "id": "aa5a315d61ae9438b18d",
  "description": "description of gist",
  "public": true,
  "owner": {
    "login": "octocat",
    "id": 1,
    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
    "gravatar_id": "",
    "url": "https://api.github.com/users/octocat",
    "html_url": "https://github.com/octocat",
    "followers_url": "https://api.github.com/users/octocat/followers",
    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
    "organizations_url": "https://api.github.com/users/octocat/orgs",
    "repos_url": "https://api.github.com/users/octocat/repos",
    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
    "received_events_url": "https://api.github.com/users/octocat/received_events",
    "type": "User",
    "site_admin": false
  },
  "user": null,
  "files": {
    "ring.erl": {
      "size": 932,
      "raw_url": "https://gist.githubusercontent.com/raw/365370/8c4d2d43d178df44f4c03a7f2ac0ff512853564e/ring.erl",
      "type": "text/plain",
      "language": "Erlang",
      "truncated": false,
      "content": "contents of gist"
    }
  },
  "truncated": false,
  "comments": 0,
  "comments_url": "https://api.github.com/gists/aa5a315d61ae9438b18d/comments/",
  "html_url": "https://gist.github.com/aa5a315d61ae9438b18d",
  "git_pull_url": "https://gist.github.com/aa5a315d61ae9438b18d.git",
  "git_push_url": "https://gist.github.com/aa5a315d61ae9438b18d.git",
  "created_at": "2010-04-14T02:15:15Z",
  "updated_at": "2011-06-20T11:34:15Z",
  "forks": [
    {
      "user": {
        "login": "octocat",
        "id": 1,
        "avatar_url": "https://github.com/images/error/octocat_happy.gif",
        "gravatar_id": "",
        "url": "https://api.github.com/users/octocat",
        "html_url": "https://github.com/octocat",
        "followers_url": "https://api.github.com/users/octocat/followers",
        "following_url": "https://api.github.com/users/octocat/following{/other_user}",
        "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
        "organizations_url": "https://api.github.com/users/octocat/orgs",
        "repos_url": "https://api.github.com/users/octocat/repos",
        "events_url": "https://api.github.com/users/octocat/events{/privacy}",
        "received_events_url": "https://api.github.com/users/octocat/received_events",
        "type": "User",
        "site_admin": false
      },
      "url": "https://api.github.com/gists/dee9c42e4998ce2ea439",
      "id": "dee9c42e4998ce2ea439",
      "created_at": "2011-04-14T16:00:49Z",
      "updated_at": "2011-04-14T16:00:49Z"
    }
  ],
  "history": [
    {
      "url": "https://api.github.com/gists/aa5a315d61ae9438b18d/57a7f021a713b1c5a6a199b54cc514735d2d462f",
      "version": "57a7f021a713b1c5a6a199b54cc514735d2d462f",
      "user": {
        "login": "octocat",
        "id": 1,
        "avatar_url": "https://github.com/images/error/octocat_happy.gif",
        "gravatar_id": "",
        "url": "https://api.github.com/users/octocat",
        "html_url": "https://github.com/octocat",
        "followers_url": "https://api.github.com/users/octocat/followers",
        "following_url": "https://api.github.com/users/octocat/following{/other_user}",
        "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
        "organizations_url": "https://api.github.com/users/octocat/orgs",
        "repos_url": "https://api.github.com/users/octocat/repos",
        "events_url": "https://api.github.com/users/octocat/events{/privacy}",
        "received_events_url": "https://api.github.com/users/octocat/received_events",
        "type": "User",
        "site_admin": false
      },
      "change_status": {
        "deletions": 0,
        "additions": 180,
        "total": 180
      },
      "committed_at": "2010-04-14T02:15:15Z"
    }
  ]
}

接下来我们将把JSON对象转换为Swift对象。首先先创建Gist类,实体对象,用来负责gists。在Xcode添加一个Swift文件,并命名为Gist。在这个文件中我们定义一个Gist类:

import Foundation

class Gist {
}

查看你的API,并构造一个你希望在表格视图中显示需要的对象模型类。

现在来看看我们需要从JSON对象中解析哪些数据。当然,我们也可以解析全部的数据,但这需要耗费很多精力,而且也没有必要这么做。后面当我们需要的时候,会从JSON中解析更多的数据。

那,我们需要显示哪些数据呢?表格视图中的单元格有标题、子标题和图像,因此,我们可以使用gist的描述、作者的GitHub的ID以及作者的头像来填充。另外,我们还需要每一个gist的唯一ID和url。所以,我们需要为JSON中的每一个gist解析出这些信息,并创建相应的Gist对象。首先我们在Gist类中添加这些属性:

class Gist {
  var id: String?
  var description: String?
  var ownerLogin: String?
  var ownerAvatarURL: String?
  var url: String?
}

对于你的模型对象,你需要决定从JSON对象中解析哪些属性显示在表格视图中。然后像Gist一样添加相应的属性。

我们希望能够通过JSON对象创建一个Gist实例,为此我们需要为类增加一个构造函数,该函数使用JSON作为参数。这里还需要引入SwiftyJSON库。同时会增加一个简单的构造函数,这样我们在没有调用GitHub API的情况下也可以创建:

import SwiftyJSON

class Gist {
  var id: String?
  var descripion: String?
  var ownerLogin: String?
  var ownerAvatarURL: String?
  var url: String?

  required init(json: JSON) {
    self.description = json["description"].string
    self.id = json["id"].string
    self.ownerLogin = json["owner"].["login"].string
    self.ownerAvatarURL = json["owner"].["avatar_url"].string
    self.url = json["url"].string
  }

  required init() {
  }
}

在你的模型对象类中创建构造函数。如果模型对象中某些属性不是字符串,请参考前面的章节来解析数字和布尔值。如果有一些属性是数组(如:gist中的文件Files)或者日期,这些属性的解析我们将在详细视图页面进行讲解。

3. 创建表格视图

现在,我们可以进行写代码了。前面我们使用Xcode创建了一个Master-Detail工程,并默认帮我们创建了一些代码。下面让我们快速看一下MasterViewController已经为我们做了哪些事情。首先是:

class MasterViewController: UITableViewController { 

  var detailViewController: DetailViewController? = nil
  var objects = [AnyObject]()

MasterViewController中有一个DetailViewController属性(该属性是我们在点击视图中的行时帮我们导航到详细页面),以及一个对象数组。在这里我们首先对象数组更改为Gists数组,这样我们就可以知道在表格视图中要展现的是哪些数据了:

class MasterViewController: UITableViewController { 

  var detailViewController: DetailViewController? = nil
  var gists = [Gist]()

参考上面将这里的数组更改为与你的App相应的名称。

接下来是:

override func viewDidLoad() {
  super.viewDidLoad()
  // Do any additional setup after loading the view, typically from a nib. 
  self.navigationItem.leftBarButtonItem = self.editButtonItem()

  let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, 
    action: "insertNewObject:")
  self.navigationItem.rightBarButtonItem = addButton 
  if let split = self.splitViewController {
    let controllers = split.viewControllers 
    self.detailViewController = (controllers[controllers.count-1] as!
      UINavigationController).topViewController as? DetailViewController
  }
}

viewDidLoad方法中往导航栏(navigation bar)中增加了两个按钮:左边增加一个编辑按钮,右边增加一个新建按钮。

通过detailViewController属性,我们就可以在详情页面中显示用户所选中gist的详细信息。

然后:

override func viewWillAppear(animated: Bool) { 
  self.clearsSelectionOnViewWillAppear = self.splitViewController!.collapsed 
  super.viewWillAppear(animated)
}

在视图显示之前,我们需要调用一下clearsSelectionOnViewWillAppear,这样就可以在我们打开其它页面时仍然保持行的选中状态。这个在iPad的分屏视图中有用,iPhone由于仅使用表格视图,所以该方法没有意义。

在视图显示的时候我们需要从GitHub中加载数据。因此,可以在viewDidAppear方法中来实现:

func loadGists() { 
  GitHubAPIManager.sharedInstance.printPublicGists()
}

override func viewDidAppear(animated: Bool) { 
  super.viewDidAppear(animated)
  loadGists()
}

通常,我们应当在viewWillAppear中来加载数据,这样视图就可以很快的显示。因为后面我们需要检查用户是否已经登录,如果没有,那么会弹出一个登录视图让用户登录,但是,如果当前视图没有显示完毕,是无法加载另外一个视图的。因此,这里我们使用viewDidAppear

创建一个类似loadGists的方法来加载你的数据。

后面我们会重构loadGists中的代码,这样就可以得到Gist的数组,并显示到视图中。

override func didReceiveMemoryWarning() { 
  super.didReceiveMemoryWarning()
  // Dispose of any resources that can be recreated.
}

假如我们有一些很重的资源文件(如:大的图片)或者一些可重建的对象,那么我们就可以在didReceiveMemoryWarning中销毁掉它们,从而能够让我们很优雅的处理低内存告警。

func insertNewObject(sender: AnyObject) {
  objects.insert(NSDate(), atIndex: 0)
  let indexPath = NSIndexPath(forRow: 0, inSection: 0) 
  self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}

新建按钮将会调用insertNewObject方法。该方法将创建一个新的对象,并把它添加到表格视图中。这个功能我们要后面很久才会实现,因此这里先弹出一个对话框告诉大家还没有实现该功能:

func insertNewObject(sender: AnyObject) {
  let alert = UIAlertController(title: "Not Implemented", message:
    "Can't create new gists yet, will implement later",
    preferredStyle: UIAlertControllerStyle.Alert)
  alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default,
    handler: nil))
  self.presentViewController(alert, animated: true, completion: nil)
}

接下来就是prepareForSegue方法,该方法将会跳转到详情页面:

// MARK: - Segues
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 
  if segue.identifier == "showDetail" {
    if let indexPath = self.tableView.indexPathForSelectedRow { 
      let object = objects[indexPath.row] as! NSDate
      let controller =
        (segue.destinationViewController as!
        UINavigationController).topViewController as! DetailViewController 
      controller.detailItem = object 
      controller.navigationItem.leftBarButtonItem =
        self.splitViewController?.displayModeButtonItem() 
      controller.navigationItem.leftItemsSupplementBackButton = true
    }
  }
}

这里,我们还是要把通用的对象替换为我们的Gists。另外,我们还需要检查一下转到的视图是否是DetailViewConroller

// MARK: - Segues
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 
  if segue.identifier == "showDetail" {
    if let indexPath = self.tableView.indexPathForSelectedRow {
      let gist = gists[indexPath.row] as Gist
      if let detailViewController = (segue.destinationViewController as!
        UINavigationController).topViewController as? 
        DetailViewController {
        detailViewController.detailItem = gist 
        detailViewController.navigationItem.leftBarButtonItem =
          self.splitViewController?.displayModeButtonItem() 
        detailViewController.navigationItem.leftItemsSupplementBackButton = true
      }
    }
  }
}

后面我们会设置详情视图中所要显示的gists。

接下来的几个方法是告诉表格视图如何进行显示:

// MARK: - Table View
override func numberOfSectionsInTableView(tableView: UITableView) -> Int { 
  return 1
}

override func tableView(tableView: UITableView, 
  numberOfRowsInSection section: Int) -> Int { 
  return objects.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath:  
  NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)  
  let object = objects[indexPath.row] as! NSDate 
  cell.textLabel!.text = object.description 
  return cell
}

再一次,我们这里需要将对象转换为gists,并且把tableView:cellForRowAIndexPath:indexPath:更改为显示gists的描述和拥有者的ID。后面再来实现如何显示拥有者的头像,因为显示图像需要额外一些处理,这里我们不想因为这个而停下来。

首先,调整故事板中的表格视图单元格,因为我们需要在上面显示两行文本:

  1. 打开mainStoryboard并选中masterViewController中的Table View
  2. 选择表格视图中单元格原型并将类型('Style')属性更改为Subtitle,这样我们就会有两个文本了
![640][]
![200][]

接下来就可以修改代码来显示Gists了:

// MARK: - Table View
override func numberOfSectionsInTableView(tableView: UITableView) -> Int { 
  return 1
}

override func tableView(tableView: UITableView, 
  numberOfRowsInSection section: Int) -> Int { 
  return gists.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath
  indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)

  let gist = gists[indexPath.row]
  cell.textLabel!.text = gist.description
  cell.detailTextLabel!.text = gist.ownerLogin
  // TODO: set cell.imageView to display image at gist.ownerAvatarURL
  return cell
}

接下来的代码就是判断gists的可编辑性:删除和创建。现在我们简化一下,先不允许进行修改:

override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath:  
  NSIndexPath) -> Bool {
  // Return false if you do not want the specified item to be editable.
  return true
}

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: 
  UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
  if editingStyle == .Delete {
    objects.removeAtIndex(indexPath.row)
    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) 
  } else if editingStyle == .Insert {
    // Create a new instance of the appropriate class, insert it into the array,
    // and add a new row to the table view.
  }
}

修改为:

override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: 
  NSIndexPath) -> Bool {
  // Return false if you do not want the specified item to be editable.
  return false
}

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: 
  UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
  if editingStyle == .Delete {
    gists.removeAtIndex(indexPath.row)
    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) 
  } else if editingStyle == .Insert {
    // Create a new instance of the appropriate class, insert it into the array,
    // and add a new row to the table view.
  }
}

现在你可以运行,但是你会发现显示的仍然是一个空白表格视图。为了测试我们可以构建一些假的本地数据而不是从GitHub上请求。修改loadGists()方法,在方法中创建一个gists数组:

func loadGists() {
  let gist1 = Gist()
  gist1.description = "The first gist" 
  gist1.ownerLogin = "gist1Owner"
  let gist2 = Gist()
  gist2.description = "The second gist" 
  gist2.ownerLogin = "gist2Owner"
  let gist3 = Gist()
  gist3.description = "The third gist" 
  gist3.ownerLogin = "gist3Owner" 
  gists = [gist1, gist2, gist3]
  // Tell the table view to reload
  self.tableView.reloadData() 
}

保存并运行,app界面如下:

![400][]

当你点击增加按钮时,会弹出一个提示框:

![400 1][]

像上面一样确保你的对象可以显示在表格视图。

现在表格视图功能应该是没有问题了,那么下面我们恢复loadGists()函数:

func loadGists() { 
  GitHubAPIManager.sharedInstance.printPublicGists()
}

4. 获取并解析API的响应

回想一下,我们在前面创建的Alamofire.Request的扩展:

public func responseObject<T: ResponseJSONObjectSerializable>

这个扩展用来处理Alamofire的响应,并将返回来的JSON格式数据转换为Swift对象(当然,相应类需要实现ResponseJSONObjectSerializable协议中的初始化方法)。现在我们需要实现的与这个很类似,只不过需要将返回的JSON数组转换为Swift对象数组。因此,我们保留这个协议,并将它添加到工程中。创建一个ResponseJSONObjectSerializable.swift文件,并把协议定义添加进去。在文件中别忘了引入SwiftyJSON库:

import Foundation
import SwiftyJSON

public protocol ResponseJSONObjectSerializable { 
  init?(json: SwiftyJSON.JSON)
}

然后修改Gist类,实现该协议(注意,我们前面已经实现了相应的构造方法):

class Gist: ResponseJSONObjectSerializable { 
  ...
}

我们也把responseObject函数拷贝进来,因为后面会使用到它。创建AlamofireRequest+JSONSerializable.swift文件,因为,它是Alamofire.Request的扩展,并且也承担了JSON的序列化处理:

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) 
}

我们的需求和这个类似,只不过返回的是[T]对象数组,而不是一个[T]对象:

extension Alamofire.Request {
  public func responseObject<T: ResponseJSONObjectSerializable>(completionHandler:
    Response<T, NSError> -> Void) -> Self {
    let serializer = ResponseSerializer<T, NSError> {
      // ...
    }

    return response(responseSerializer: serializer, completionHandler: completionHandler) 
  }

  public func responseArray<T: ResponseJSONObjectSerializable>(completionHandler: 
    Response<[T], NSError> -> Void) -> Self {
    let serializer = ResponseSerializer<[T], NSError> {
      // ...
    }

    return response(responseSerializer: serializer, completionHandler: completionHandler) 
  }
}

具体实现也很类似:

public func responseArray<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) 
      var objects: [T] = []
      for (_, item) in json {
        if let object = T(json: item) { 
          objects.append(object)
        }
      }
      return .Success(objects) 
    case .Failure(let error):
      return .Failure(error) 
    }
  }

  return response(responseSerializer: serializer, completionHandler: completionHandler) 
}

最大的不同点就是我们循环json中的每个元素for (_, item) in json,并为它创建相应的对象:let object = T(json: item)。如果对象创建成功则把它添加到数组中。

现在,我们需要:

  1. 完成我们的函数使其获取公共的gists,并把返回值解析为一个数组
  2. 将函数更改为返回gists数组并传给表格视图

getPublicGists函数看起来很像之前的printPulicGists

func printPublicGists() -> Void { 
  Alamofire.request(GistRouter.GetPublic())
    .responseString { response in
      if let receivedString = response.result.value {
        print(receivedString)
      }
    }
}

最大的不同就是将打印替换为返回一个数组。因此,我们把responseString替换为responseArray。现在我们可以把这个函数加入到GitHubAPIManager中了:

func getPublicGists() -> Void { 
  Alamofire.request(GistRouter.GetPublic())
    .responseArray {
      ...
  }
}

这看起来有点奇怪。我们前面不是说要返回一个数组么,但这里返回的是Void啊。嗯,是的,这是因为API的调用是异步的,我们发起一个请求,然后当请求处理完毕后我们会收到一个通知。我们可以将这个处理放在完成处理程序中。下面我们来添加一个块代码,这样当请求处理完毕后就可以调用了。我们的完成处理程序需要处理两种可能:一种情况是正确返回了一个Gists数组,另外一种情况就是返回了一个错误。

完成处理程序的签名是(Result<T>, NSError)。它是Alamofire所创建的一个指定对象,这样可以让我们在.Success情况下返回一个Gists数组,在.Failure情况下返回一个错误。

因此我们发送请求后,将响应序列化器(response serializer)设置为我们上面所创建的responseArray。然后在调用成功后返回Gists数组,或者失败时返回一个错误:

func getPublicGists(completionHandler: (Result<[Gist], NSError>) -> Void) {
  Alamofire.request(.GET, "https://api.github.com/gists/public")
    .responseArray { (response:Response<[Gist], NSError>) in 
      ...
  }
}

因为responseArray的完成处理程序返回的数组参数中是一个泛型对象,因此我们需要修改让它明确返回的对象类型,因此将参数修改为:Respoonse<[Gist], NSError>。否则当数据返回时将不知道如何创建相应的对象。

getPublicGists的完成处理程序匹配responseArray中的一个,并不需要在这里处理任何错误。因此,在.responseArray的块(block)中只需要调用完成处理程序即可。对于错误的处理则是调用者需要关心的事:

func getPublicGists(completionHandler: (Result<[Gist], NSError>) -> Void){ 
  Alamofire.request(GistRouter.GetPublic())
    .responseArray { (response:Response<[Gist], NSError>) in 
      completionHandler(response.result)
  } 
}

创建一个函数像getPublicGists一样,返回你的业务对象数组。

Ok,现在让我们看看我们需要在什么时候调用getPublicGists。先看看之前在MasterViewController是在如何调用的:

func loadGists() { 
  GitHubAPIManager.sharedInstance.printPublicGists()
}

override func viewDidAppear(animated: Bool) { 
  super.viewDidAppear(animated)
  loadGists()
}

看来最好是将printPublicGists()替换为getPublicGist。这个非常容易做:

func loadGists() { 
  GitHubAPIManager.sharedInstance.getPublicGists() { result in
    guard result.error == nil else { 
      print(result.error)
      // TODO: display error
      return
    }

    if let fetchedGists = result.value { 
      self.gists = fetchedGists  
    }
    self.tableView.reloadData()
  }
}

我们发起一个异步调用,并返回gists数组。如果成功调用,我们会把gists保存到本地数组变量中,并告诉表格视图使用新的数据刷新显示。简单漂亮!

创建你的loadGists方法,并调用之前你的getPublicGists方法,把返回的结果保存到数组对象中,这样你的MasterViewConroller就可以将它们显示到表格视图中了。

现在API调用和表格视图已经很好的整合了。保存并运行看看效果。

![400 2][]

小结

到这里我们已经完成了app的核心功能。下面我们逐步添加以下功能:

  • 在单元格中显示图片
  • 当滚动时加载更多Gists
  • 下拉刷新
  • Gists的详细视图
  • 删除Gists
  • 新建Gists

GitHub上本章的代码:tableview