本文将继续前面的教程,继续讲解如何通过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 Foundation
import Alamofire
import SwiftyJSON
class GitHubAPIManager {
}
如果你是使用的是其它API代码,那么最好这里将名称更改为合适的名称,而不是
GitHubAPIManager
。
当你与API打交道的时候,通常我们会得到的是一堆代码,而不是一个对象。我们需要设置自定义报头,跟踪OAuth访问令牌,处理client secrets
和client 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)
}
}
}
为了可以测试该代码,你需要修改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 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。后面再来实现如何显示拥有者的头像,因为显示图像需要额外一些处理,这里我们不想因为这个而停下来。
首先,调整故事板中的表格视图单元格,因为我们需要在上面显示两行文本:
- 打开
mainStoryboard
并选中masterViewController
中的Table View
- 选择表格视图中单元格原型并将类型('Style')属性更改为
Subtitle
,这样我们就会有两个文本了
<div style="text-align: center">
<img src="http://o6pjjbgcb.bkt.clouddn.com/rest_020.png?imageView2/0/h/640" style="width:600px"/>
</div>
<div style="text-align: center">
<img src="http://o6pjjbgcb.bkt.clouddn.com/rest_030.png?imageView2/0/h/200" style="height:140px"/>
</div>
接下来就可以修改代码来显示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界面如下:
<div style="text-align: center">
<img src="http://o6pjjbgcb.bkt.clouddn.com/rest_040.png?imageView2/0/w/400" style="width:320px"/>
</div>
当你点击增加按钮时,会弹出一个提示框:
<div style="text-align: center">
<img src="http://o6pjjbgcb.bkt.clouddn.com/rest_050.png?imageView2/0/w/400" style="width:320px"/>
</div>
像上面一样确保你的对象可以显示在表格视图。
现在表格视图功能应该是没有问题了,那么下面我们恢复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)
。如果对象创建成功则把它添加到数组中。
现在,我们需要:
- 完成我们的函数使其获取公共的gists,并把返回值解析为一个数组
- 将函数更改为返回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调用和表格视图已经很好的整合了。保存并运行看看效果。
<div style="text-align: center">
<img src="http://o6pjjbgcb.bkt.clouddn.com/rest_060.png?imageView2/0/w/400" style="width:320px"/>
</div>
小结
到这里我们已经完成了app的核心功能。下面我们逐步添加以下功能:
- 在单元格中显示图片
- 当滚动时加载更多Gists
- 下拉刷新
- Gists的详细视图
- 删除Gists
- 新建Gists
GitHub上本章的代码:tableview。