本文將繼續(xù)前面的教程,繼續(xù)講解如何通過REST API獲取數(shù)據(jù)列表并解析為Swift對象,然后顯示在表格視圖中。
重要說明: 這是一個系列教程,非本人原創(chuàng),而是翻譯國外的一個教程。本人也在學(xué)習(xí)Swift,看到這個教程對開發(fā)一個實際的APP非常有幫助,所以翻譯共享給大家。原教程非常長,我會陸續(xù)翻譯并發(fā)布,歡迎交流與分享。
為什么使用像Alamofire這樣的庫
關(guān)于編程中最難的兩件事情有一堆的笑話。有人說最難的事情是命名、評估和off-by-one(譯者注:off-by-one大小差一錯誤是程序設(shè)計中常見錯誤,具體可以參考這里off-by-one)錯誤。還有人說是評估和拿到回報。但我認(rèn)為是固化需求,固化需求可以讓你知道哪些東西需要做,并能夠讓你保持代碼在同一級別上抽象。
那么什么是在同一級別上抽象呢?先讓我們看看一些古老的,讓人迷糊的Objective-C代碼:
NSArray *myGists = [[NSArray alloc]] initWithObjects:
[NSString stringWithString:@"text of gist 1"],
[NSString stringWithString:@"text of gist 2"],
nil];
// 使用myGists進(jìn)行某些處理
[myGists release];
這段代碼的核心功能就是想對Gists數(shù)組進(jìn)行某些處理。但是對于程序員來說,這里想的不僅僅是gists,還要考慮內(nèi)存的管理(如:分配alloc、釋放release)。因此,對于他們來說,在腦中要同時處理2個不同層次的抽象。這些對象不僅是Gists對象,它們還是內(nèi)存中的塊。
當(dāng)然,所有的Gists對象都是內(nèi)存中的塊。而且某個地方的代碼也是這么去處理。但,它還是不應(yīng)該與gists的業(yè)務(wù)操作,如收藏、編輯,在同一個地方。這也會把web service的調(diào)用混在一起了:
- 一部分代碼需要知道并處理底層的網(wǎng)絡(luò)事務(wù)
- 一部分代碼要處理JSON
- 還有一部分代碼要處理gists(或你的對象)
這是三個層次的抽象,它們不需要(也不應(yīng)該)混在一起。在同一層次的抽象上編碼,要比在不同層次上來回切換理解代碼要輕松的多。
你可以不需要像SwiftyJSON、Alamofire這樣的庫。但它們的確能把底層處理封裝的更好。而且一旦它們開源,你還可以在需要的時候?qū)Υa進(jìn)行調(diào)整修改,而你又會失去什么呢?
連接REST API和表格視圖
UITableView控件是iOS應(yīng)用中常用的控件。結(jié)合Web Service,就是很多App的核心業(yè)務(wù)功能,如:郵件、Twitter及Facebook,甚至蘋果自己的備忘錄,連App Store也是一樣。
接下來我們將新建一個Xcode工程,通過GitHub的gists API獲取數(shù)據(jù)。然后在表格視圖中顯示公共的gists列表。由此,我們將發(fā)起一個GET請求,并將返回的數(shù)據(jù)解析為JSON格式,然后讓表格視圖顯示這些結(jié)果。
本章重點(diǎn)講的是如何把API返回的數(shù)據(jù)綁定到表格視圖中,不會涉及如何將
UITableView控件添加到Swift應(yīng)用這種基礎(chǔ)知識。如果你對如何使用UITableView控件有困惑,請參考Apple's docs或者這個教程。
如果你不想自己敲代碼,請到GitHub下載本章的代碼。
1. 創(chuàng)建Swift工程
我們終于可以動手創(chuàng)建GitHub Gists應(yīng)用了。首先我們需要在Xcode中創(chuàng)建一個工程:
啟動Xcode。
創(chuàng)建一個master-detail類型Swift工程(Devices中你可以選擇universal或者iPhone)。確保在創(chuàng)建工程時選擇使用Swift語言,并且沒有選中Core Data選項。
使用CoclaPods將Alamofire 3.1和SwoftyJSON 2.3添加到工程中(如果不知道如何做,請參考這里)。
然后,打開類型為.xcworkspace文件。
由于我們現(xiàn)在還使用不到由Xcode生成的樣板代碼,先不要管它,后面我們涉及到的時候會來解釋。這里你唯一需要注意的是,Xcode創(chuàng)建了兩個視圖控制器:一個表格視圖控制器MasterViewController,和一個detailViewController詳細(xì)頁面視圖控制器。而它們正好可以用來顯示我們gists的列表和gist的詳細(xì)信息。接下來幾章我們都會與MasterViewController打交道。
創(chuàng)建一個新文件并命名為:GitHubAPIManager.swift。這個類將負(fù)責(zé)與API之間的處理,也可以稱為API管理器。它可以幫我們把代碼組織的更好,也避免使視圖控制器的代碼變成一個龐大的文件。同時,也方便我們可以在多個視圖控制器之間共享代碼。
在文件的頭部,引入Alamofire和SwiftyJSON:
import Foundation
import Alamofire
import SwiftyJSON
class GitHubAPIManager {
}
如果你是使用的是其它API代碼,那么最好這里將名稱更改為合適的名稱,而不是
GitHubAPIManager。
當(dāng)你與API打交道的時候,通常我們會得到的是一堆代碼,而不是一個對象。我們需要設(shè)置自定義報頭,跟蹤OAuth訪問令牌,處理client secrets和client ID,處理認(rèn)證或者其它常見的錯誤。為了將這些代碼從App Delegate及我們的模型對象中分離,我們將會把它們統(tǒng)一到GitHubAPIManager中進(jìn)行管理。
在本教程的實例中我們只與GitHub API打交道,所以這里只有一個API管理器。因此我們在該類中聲明一個sharedInstance變量,這樣其它調(diào)用者就可以通過它來獲取GitHubAPIManager的唯一實例:
import Foundation
import Alamofire
import SwiftyJSON
class GitHubAPIManager {
static let sharedInstance = GitHubAPIManager()
}
接下來我們就可以通過API請求獲取不需認(rèn)證的公共gists列表了。為了方便我們快速理解,這里當(dāng)我們獲取API請求結(jié)果后先在控制臺打印出來。然后我們再把它和表格視圖集成。
因此,我們先聲明這個簡單的方法:
class GitHubAPIManager {
...
func printPublicGists() -> Void {
// TODO: 待實現(xiàn)
}
}
接下來讓我們創(chuàng)建Router路由,并把新建的文件命名為:GistRouter.swift。該路由器將負(fù)責(zé)創(chuàng)建URL請求,從而能夠讓我們的API管理器保持簡單。新建的路由器和前面類似,除了只有一個獲取公共gists的GET調(diào)用:
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方法。該方法在每次主視圖顯示的時候都會調(diào)用:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// 開始測試
GitHubAPIManager.sharedInstance.printPublicGists()
// 結(jié)束測試
}
保存并運(yùn)行。在模擬器或者你的手機(jī)上你將看到一個空的表格視圖。但,如果API調(diào)用成功,你會在屏幕的底部(控制臺)看到打印出的JSON數(shù)據(jù):
"[{\\"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類似的方法。它將獲取到一個對象數(shù)組,并在控制臺中打印。
2. 解析API返回的JSON數(shù)據(jù)
API調(diào)用返回了一個包含gists數(shù)組的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對象轉(zhuǎn)換為Swift對象。首先先創(chuàng)建Gist類,實體對象,用來負(fù)責(zé)gists。在Xcode添加一個Swift文件,并命名為Gist。在這個文件中我們定義一個Gist類:
import Foundation
class Gist {
}
查看你的API,并構(gòu)造一個你希望在表格視圖中顯示需要的對象模型類。
現(xiàn)在來看看我們需要從JSON對象中解析哪些數(shù)據(jù)。當(dāng)然,我們也可以解析全部的數(shù)據(jù),但這需要耗費(fèi)很多精力,而且也沒有必要這么做。后面當(dāng)我們需要的時候,會從JSON中解析更多的數(shù)據(jù)。
那,我們需要顯示哪些數(shù)據(jù)呢?表格視圖中的單元格有標(biāo)題、子標(biāo)題和圖像,因此,我們可以使用gist的描述、作者的GitHub的ID以及作者的頭像來填充。另外,我們還需要每一個gist的唯一ID和url。所以,我們需要為JSON中的每一個gist解析出這些信息,并創(chuàng)建相應(yīng)的Gist對象。首先我們在Gist類中添加這些屬性:
class Gist {
var id: String?
var description: String?
var ownerLogin: String?
var ownerAvatarURL: String?
var url: String?
}
對于你的模型對象,你需要決定從JSON對象中解析哪些屬性顯示在表格視圖中。然后像
Gist一樣添加相應(yīng)的屬性。
我們希望能夠通過JSON對象創(chuàng)建一個Gist實例,為此我們需要為類增加一個構(gòu)造函數(shù),該函數(shù)使用JSON作為參數(shù)。這里還需要引入SwiftyJSON庫。同時會增加一個簡單的構(gòu)造函數(shù),這樣我們在沒有調(diào)用GitHub API的情況下也可以創(chuàng)建:
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() {
}
}
在你的模型對象類中創(chuàng)建構(gòu)造函數(shù)。如果模型對象中某些屬性不是字符串,請參考前面的章節(jié)來解析數(shù)字和布爾值。如果有一些屬性是數(shù)組(如:gist中的文件
Files)或者日期,這些屬性的解析我們將在詳細(xì)視圖頁面進(jìn)行講解。
3. 創(chuàng)建表格視圖
現(xiàn)在,我們可以進(jìn)行寫代碼了。前面我們使用Xcode創(chuàng)建了一個Master-Detail工程,并默認(rèn)幫我們創(chuàng)建了一些代碼。下面讓我們快速看一下MasterViewController已經(jīng)為我們做了哪些事情。首先是:
class MasterViewController: UITableViewController {
var detailViewController: DetailViewController? = nil
var objects = [AnyObject]()
在MasterViewController中有一個DetailViewController屬性(該屬性是我們在點(diǎn)擊視圖中的行時幫我們導(dǎo)航到詳細(xì)頁面),以及一個對象數(shù)組。在這里我們首先對象數(shù)組更改為Gists數(shù)組,這樣我們就可以知道在表格視圖中要展現(xiàn)的是哪些數(shù)據(jù)了:
class MasterViewController: UITableViewController {
var detailViewController: DetailViewController? = nil
var gists = [Gist]()
參考上面將這里的數(shù)組更改為與你的App相應(yīng)的名稱。
接下來是:
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方法中往導(dǎo)航欄(navigation bar)中增加了兩個按鈕:左邊增加一個編輯按鈕,右邊增加一個新建按鈕。
通過detailViewController屬性,我們就可以在詳情頁面中顯示用戶所選中g(shù)ist的詳細(xì)信息。
然后:
override func viewWillAppear(animated: Bool) {
self.clearsSelectionOnViewWillAppear = self.splitViewController!.collapsed
super.viewWillAppear(animated)
}
在視圖顯示之前,我們需要調(diào)用一下clearsSelectionOnViewWillAppear,這樣就可以在我們打開其它頁面時仍然保持行的選中狀態(tài)。這個在iPad的分屏視圖中有用,iPhone由于僅使用表格視圖,所以該方法沒有意義。
在視圖顯示的時候我們需要從GitHub中加載數(shù)據(jù)。因此,可以在viewDidAppear方法中來實現(xiàn):
func loadGists() {
GitHubAPIManager.sharedInstance.printPublicGists()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
loadGists()
}
通常,我們應(yīng)當(dāng)在
viewWillAppear中來加載數(shù)據(jù),這樣視圖就可以很快的顯示。因為后面我們需要檢查用戶是否已經(jīng)登錄,如果沒有,那么會彈出一個登錄視圖讓用戶登錄,但是,如果當(dāng)前視圖沒有顯示完畢,是無法加載另外一個視圖的。因此,這里我們使用viewDidAppear。
創(chuàng)建一個類似
loadGists的方法來加載你的數(shù)據(jù)。
后面我們會重構(gòu)loadGists中的代碼,這樣就可以得到Gist的數(shù)組,并顯示到視圖中。
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
假如我們有一些很重的資源文件(如:大的圖片)或者一些可重建的對象,那么我們就可以在didReceiveMemoryWarning中銷毀掉它們,從而能夠讓我們很優(yōu)雅的處理低內(nèi)存告警。
func insertNewObject(sender: AnyObject) {
objects.insert(NSDate(), atIndex: 0)
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
新建按鈕將會調(diào)用insertNewObject方法。該方法將創(chuàng)建一個新的對象,并把它添加到表格視圖中。這個功能我們要后面很久才會實現(xiàn),因此這里先彈出一個對話框告訴大家還沒有實現(xiàn)該功能:
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方法,該方法將會跳轉(zhuǎn)到詳情頁面:
// 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。另外,我們還需要檢查一下轉(zhuǎn)到的視圖是否是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
}
}
}
}
后面我們會設(shè)置詳情視圖中所要顯示的gists。
接下來的幾個方法是告訴表格視圖如何進(jìn)行顯示:
// 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
}
再一次,我們這里需要將對象轉(zhuǎn)換為gists,并且把tableView:cellForRowAIndexPath:indexPath:更改為顯示gists的描述和擁有者的ID。后面再來實現(xiàn)如何顯示擁有者的頭像,因為顯示圖像需要額外一些處理,這里我們不想因為這個而停下來。
首先,調(diào)整故事板中的表格視圖單元格,因為我們需要在上面顯示兩行文本:
- 打開
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的可編輯性:刪除和創(chuàng)建?,F(xiàn)在我們簡化一下,先不允許進(jìn)行修改:
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.
}
}
現(xiàn)在你可以運(yùn)行,但是你會發(fā)現(xiàn)顯示的仍然是一個空白表格視圖。為了測試我們可以構(gòu)建一些假的本地數(shù)據(jù)而不是從GitHub上請求。修改loadGists()方法,在方法中創(chuàng)建一個gists數(shù)組:
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()
}
保存并運(yùn)行,app界面如下:
<div style="text-align: center">
<img src="http://o6pjjbgcb.bkt.clouddn.com/rest_040.png?imageView2/0/w/400" style="width:320px"/>
</div>
當(dāng)你點(diǎn)擊增加按鈕時,會彈出一個提示框:
<div style="text-align: center">
<img src="http://o6pjjbgcb.bkt.clouddn.com/rest_050.png?imageView2/0/w/400" style="width:320px"/>
</div>
像上面一樣確保你的對象可以顯示在表格視圖。
現(xiàn)在表格視圖功能應(yīng)該是沒有問題了,那么下面我們恢復(fù)loadGists()函數(shù):
func loadGists() {
GitHubAPIManager.sharedInstance.printPublicGists()
}
4. 獲取并解析API的響應(yīng)
回想一下,我們在前面創(chuàng)建的Alamofire.Request的擴(kuò)展:
public func responseObject<T: ResponseJSONObjectSerializable>
這個擴(kuò)展用來處理Alamofire的響應(yīng),并將返回來的JSON格式數(shù)據(jù)轉(zhuǎn)換為Swift對象(當(dāng)然,相應(yīng)類需要實現(xiàn)ResponseJSONObjectSerializable協(xié)議中的初始化方法)。現(xiàn)在我們需要實現(xiàn)的與這個很類似,只不過需要將返回的JSON數(shù)組轉(zhuǎn)換為Swift對象數(shù)組。因此,我們保留這個協(xié)議,并將它添加到工程中。創(chuàng)建一個ResponseJSONObjectSerializable.swift文件,并把協(xié)議定義添加進(jìn)去。在文件中別忘了引入SwiftyJSON庫:
import Foundation
import SwiftyJSON
public protocol ResponseJSONObjectSerializable {
init?(json: SwiftyJSON.JSON)
}
然后修改Gist類,實現(xiàn)該協(xié)議(注意,我們前面已經(jīng)實現(xiàn)了相應(yīng)的構(gòu)造方法):
class Gist: ResponseJSONObjectSerializable {
...
}
我們也把responseObject函數(shù)拷貝進(jìn)來,因為后面會使用到它。創(chuàng)建AlamofireRequest+JSONSerializable.swift文件,因為,它是Alamofire.Request的擴(kuò)展,并且也承擔(dān)了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 = "無法進(jìn)行對象序列化,因為輸入的數(shù)據(jù)為空。"
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創(chuàng)建對象"
let error = Error.errorWithCode(.JSONSerializationFailed,
failureReason: failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: serializer, completionHandler: completionHandler)
}
我們的需求和這個類似,只不過返回的是[T]對象數(shù)組,而不是一個[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)
}
}
具體實現(xiàn)也很類似:
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 = "無法解析為數(shù)組,因為輸入的數(shù)據(jù)為空。"
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)
}
最大的不同點(diǎn)就是我們循環(huán)json中的每個元素for (_, item) in json,并為它創(chuàng)建相應(yīng)的對象:let object = T(json: item)。如果對象創(chuàng)建成功則把它添加到數(shù)組中。
現(xiàn)在,我們需要:
- 完成我們的函數(shù)使其獲取公共的gists,并把返回值解析為一個數(shù)組
- 將函數(shù)更改為返回gists數(shù)組并傳給表格視圖
getPublicGists函數(shù)看起來很像之前的printPulicGists:
func printPublicGists() -> Void {
Alamofire.request(GistRouter.GetPublic())
.responseString { response in
if let receivedString = response.result.value {
print(receivedString)
}
}
}
最大的不同就是將打印替換為返回一個數(shù)組。因此,我們把responseString替換為responseArray?,F(xiàn)在我們可以把這個函數(shù)加入到GitHubAPIManager中了:
func getPublicGists() -> Void {
Alamofire.request(GistRouter.GetPublic())
.responseArray {
...
}
}
這看起來有點(diǎn)奇怪。我們前面不是說要返回一個數(shù)組么,但這里返回的是Void啊。嗯,是的,這是因為API的調(diào)用是異步的,我們發(fā)起一個請求,然后當(dāng)請求處理完畢后我們會收到一個通知。我們可以將這個處理放在完成處理程序中。下面我們來添加一個塊代碼,這樣當(dāng)請求處理完畢后就可以調(diào)用了。我們的完成處理程序需要處理兩種可能:一種情況是正確返回了一個Gists數(shù)組,另外一種情況就是返回了一個錯誤。
完成處理程序的簽名是(Result<T>, NSError)。它是Alamofire所創(chuàng)建的一個指定對象,這樣可以讓我們在.Success情況下返回一個Gists數(shù)組,在.Failure情況下返回一個錯誤。
因此我們發(fā)送請求后,將響應(yīng)序列化器(response serializer)設(shè)置為我們上面所創(chuàng)建的responseArray。然后在調(diào)用成功后返回Gists數(shù)組,或者失敗時返回一個錯誤:
func getPublicGists(completionHandler: (Result<[Gist], NSError>) -> Void) {
Alamofire.request(.GET, "https://api.github.com/gists/public")
.responseArray { (response:Response<[Gist], NSError>) in
...
}
}
因為responseArray的完成處理程序返回的數(shù)組參數(shù)中是一個泛型對象,因此我們需要修改讓它明確返回的對象類型,因此將參數(shù)修改為:Respoonse<[Gist], NSError>。否則當(dāng)數(shù)據(jù)返回時將不知道如何創(chuàng)建相應(yīng)的對象。
getPublicGists的完成處理程序匹配responseArray中的一個,并不需要在這里處理任何錯誤。因此,在.responseArray的塊(block)中只需要調(diào)用完成處理程序即可。對于錯誤的處理則是調(diào)用者需要關(guān)心的事:
func getPublicGists(completionHandler: (Result<[Gist], NSError>) -> Void){
Alamofire.request(GistRouter.GetPublic())
.responseArray { (response:Response<[Gist], NSError>) in
completionHandler(response.result)
}
}
創(chuàng)建一個函數(shù)像
getPublicGists一樣,返回你的業(yè)務(wù)對象數(shù)組。
Ok,現(xiàn)在讓我們看看我們需要在什么時候調(diào)用getPublicGists。先看看之前在MasterViewController是在如何調(diào)用的:
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()
}
}
我們發(fā)起一個異步調(diào)用,并返回gists數(shù)組。如果成功調(diào)用,我們會把gists保存到本地數(shù)組變量中,并告訴表格視圖使用新的數(shù)據(jù)刷新顯示。簡單漂亮!
創(chuàng)建你的
loadGists方法,并調(diào)用之前你的getPublicGists方法,把返回的結(jié)果保存到數(shù)組對象中,這樣你的MasterViewConroller就可以將它們顯示到表格視圖中了。
現(xiàn)在API調(diào)用和表格視圖已經(jīng)很好的整合了。保存并運(yùn)行看看效果。
<div style="text-align: center">
<img src="http://o6pjjbgcb.bkt.clouddn.com/rest_060.png?imageView2/0/w/400" style="width:320px"/>
</div>
小結(jié)
到這里我們已經(jīng)完成了app的核心功能。下面我們逐步添加以下功能:
- 在單元格中顯示圖片
- 當(dāng)滾動時加載更多Gists
- 下拉刷新
- Gists的詳細(xì)視圖
- 刪除Gists
- 新建Gists
GitHub上本章的代碼:tableview。