(Swift) iOS Apps with REST APIs(五) -- 整合REST API和表格视图

来源:互联网 发布:python做数据分析应用 编辑:程序博客网 时间:2024/05/24 06:01

本文将继续前面的教程,继续讲解如何通过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.1和SwoftyJSON 2.3添加到工程中(如果不知道如何做,请参考这里)。

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

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

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

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

import Foundationimport Alamofireimport SwiftyJSONclass GitHubAPIManager {}

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

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

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

import Foundationimport Alamofireimport SwiftyJSONclass GitHubAPIManager {  static let sharedInstance = GitHubAPIManager()} 
接下来我们就可以通过API请求获取不需认证的公共gists列表了。为了方便我们快速理解,这里当我们获取API请求结果后先在控制台打印出来。然后我们再把它和表格视图集成。因此,我们先声明这个简单的方法:
class GitHubAPIManager {  ...  func printPublicGists() -> Void {    // TODO: 待实现  }}
接下来让我们创建`Router`路由,并把新建的文件命名为:`GistRouter.swift`。该路由器将负责创建URL请求,从而能够让我们的API管理器保持简单。新建的路由器和前面类似,除了只有一个获取公共gists的`GET`调用:
import Foundationimport Alamofireenum 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)      }  }}
为了可以测试该代码,你需要修改`MasterViewController`的`viewDidAppear`方法。该方法在每次主视图显示的时候都会调用:
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 Foundationclass 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 SwiftyJSONclass 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: - Seguesoverride 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: - Seguesoverride 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 Viewoverride 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`,这样我们就会有两个文本了

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

// MARK: - Table Viewoverride 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界面如下:

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

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

现在表格视图功能应该是没有问题了,那么下面我们恢复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 Foundationimport SwiftyJSONpublic 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, 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
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调用和表格视图已经很好的整合了。保存并运行看看效果。

小结

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

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

GitHub上本章的代码:tableview。

原创粉丝点击