(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 secrets
和client 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`的完成处理程序返回的数组参数中是一个泛型对象,因此我们需要修改让它明确返回的对象类型,因此将参数修改为:`Respoonsefunc 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。
- (Swift) iOS Apps with REST APIs(五) -- 整合REST API和表格视图
- (Swift) iOS Apps with REST APIs(三) -- 使用Alamofire和SwiftyJSON进行REST API调用
- (Swift) iOS Apps with REST APIs(一)
- (Swift) iOS Apps with REST APIs(二)
- (Swift) iOS Apps with REST APIs(四) -- 自定义Alamofire的响应序列化
- (Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存
- Securing REST APIs With Spring Boot
- SiteWhere REST APIs
- YARN REST APIs
- Spark for REST APIs
- 9.springboot整合swagger2生成rest-api
- REST API
- REST api
- rest api
- REST API
- REST API
- Rest API
- Secure REST API with oauth2 (翻译)
- Code Size
- 小程序访问本地服务器注意点
- nvalid bound statement (not found)解决方法
- 大数据相关
- C语言指针
- (Swift) iOS Apps with REST APIs(五) -- 整合REST API和表格视图
- Java类的设计模式综述
- 漫步最优化十四——凸函数与凹函数
- 浅谈对spring的理解
- acdream A
- live555搭建简易流媒体服务
- java内存回收机制相关
- 扑克牌与一副扑克牌
- (Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存