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

本文將繼續(xù)前面的教程,繼續(xù)講解如何通過REST API獲取數(shù)據(jù)列表并解析為Swift對(duì)象宗雇,然后顯示在表格視圖中插爹。

重要說明: 這是一個(gè)系列教程,非本人原創(chuàng)吐根,而是翻譯國(guó)外的一個(gè)教程正歼。本人也在學(xué)習(xí)Swift,看到這個(gè)教程對(duì)開發(fā)一個(gè)實(shí)際的APP非常有幫助拷橘,所以翻譯共享給大家局义。原教程非常長(zhǎng),我會(huì)陸續(xù)翻譯并發(fā)布冗疮,歡迎交流與分享萄唇。

為什么使用像Alamofire這樣的庫(kù)

關(guān)于編程中最難的兩件事情有一堆的笑話。有人說最難的事情是命名术幔、評(píng)估和off-by-one(譯者注:off-by-one大小差一錯(cuò)誤是程序設(shè)計(jì)中常見錯(cuò)誤另萤,具體可以參考這里off-by-one)錯(cuò)誤。還有人說是評(píng)估和拿到回報(bào)诅挑。但我認(rèn)為是固化需求四敞,固化需求可以讓你知道哪些東西需要做,并能夠讓你保持代碼在同一級(jí)別上抽象揍障。

那么什么是在同一級(jí)別上抽象呢目养?先讓我們看看一些古老的,讓人迷糊的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];

這段代碼的核心功能就是想對(duì)Gists數(shù)組進(jìn)行某些處理毒嫡。但是對(duì)于程序員來說癌蚁,這里想的不僅僅是gists,還要考慮內(nèi)存的管理(如:分配alloc兜畸、釋放release)努释。因此,對(duì)于他們來說咬摇,在腦中要同時(shí)處理2個(gè)不同層次的抽象伐蒂。這些對(duì)象不僅是Gists對(duì)象,它們還是內(nèi)存中的塊肛鹏。

當(dāng)然逸邦,所有的Gists對(duì)象都是內(nèi)存中的塊。而且某個(gè)地方的代碼也是這么去處理在扰。但缕减,它還是不應(yīng)該與gists的業(yè)務(wù)操作,如收藏芒珠、編輯桥狡,在同一個(gè)地方。這也會(huì)把web service的調(diào)用混在一起了:

  • 一部分代碼需要知道并處理底層的網(wǎng)絡(luò)事務(wù)
  • 一部分代碼要處理JSON
  • 還有一部分代碼要處理gists(或你的對(duì)象)

這是三個(gè)層次的抽象,它們不需要(也不應(yīng)該)混在一起裹芝。在同一層次的抽象上編碼部逮,要比在不同層次上來回切換理解代碼要輕松的多。

你可以不需要像SwiftyJSON嫂易、Alamofire這樣的庫(kù)兄朋。但它們的確能把底層處理封裝的更好。而且一旦它們開源炬搭,你還可以在需要的時(shí)候?qū)Υa進(jìn)行調(diào)整修改蜈漓,而你又會(huì)失去什么呢?

連接REST API和表格視圖

UITableView控件是iOS應(yīng)用中常用的控件宫盔。結(jié)合Web Service,就是很多App的核心業(yè)務(wù)功能享完,如:郵件灼芭、Twitter及Facebook,甚至蘋果自己的備忘錄般又,連App Store也是一樣彼绷。

接下來我們將新建一個(gè)Xcode工程,通過GitHub的gists API獲取數(shù)據(jù)茴迁。然后在表格視圖中顯示公共的gists列表寄悯。由此,我們將發(fā)起一個(gè)GET請(qǐng)求堕义,并將返回的數(shù)據(jù)解析為JSON格式猜旬,然后讓表格視圖顯示這些結(jié)果。

本章重點(diǎn)講的是如何把API返回的數(shù)據(jù)綁定到表格視圖中倦卖,不會(huì)涉及如何將UITableView控件添加到Swift應(yīng)用這種基礎(chǔ)知識(shí)洒擦。如果你對(duì)如何使用UITableView控件有困惑,請(qǐng)參考Apple's docs或者這個(gè)教程怕膛。

如果你不想自己敲代碼熟嫩,請(qǐng)到GitHub下載本章的代碼

1. 創(chuàng)建Swift工程

我們終于可以動(dòng)手創(chuàng)建GitHub Gists應(yīng)用了褐捻。首先我們需要在Xcode中創(chuàng)建一個(gè)工程:

啟動(dòng)Xcode掸茅。

創(chuàng)建一個(gè)master-detail類型Swift工程(Devices中你可以選擇universal或者iPhone)。確保在創(chuàng)建工程時(shí)選擇使用Swift語(yǔ)言柠逞,并且沒有選中Core Data選項(xiàng)昧狮。

使用CoclaPods將Alamofire 3.1SwoftyJSON 2.3添加到工程中(如果不知道如何做,請(qǐng)參考這里)边苹。

然后陵且,打開類型為.xcworkspace文件。

由于我們現(xiàn)在還使用不到由Xcode生成的樣板代碼,先不要管它慕购,后面我們涉及到的時(shí)候會(huì)來解釋聊疲。這里你唯一需要注意的是,Xcode創(chuàng)建了兩個(gè)視圖控制器:一個(gè)表格視圖控制器MasterViewController沪悲,和一個(gè)detailViewController詳細(xì)頁(yè)面視圖控制器获洲。而它們正好可以用來顯示我們gists的列表和gist的詳細(xì)信息。接下來幾章我們都會(huì)與MasterViewController打交道殿如。

創(chuàng)建一個(gè)新文件并命名為:GitHubAPIManager.swift贡珊。這個(gè)類將負(fù)責(zé)與API之間的處理,也可以稱為API管理器涉馁。它可以幫我們把代碼組織的更好门岔,也避免使視圖控制器的代碼變成一個(gè)龐大的文件。同時(shí)烤送,也方便我們可以在多個(gè)視圖控制器之間共享代碼寒随。

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

import Foundation
import Alamofire
import SwiftyJSON
  
class GitHubAPIManager {

}

如果你是使用的是其它API代碼帮坚,那么最好這里將名稱更改為合適的名稱妻往,而不是GitHubAPIManager

當(dāng)你與API打交道的時(shí)候试和,通常我們會(huì)得到的是一堆代碼讯泣,而不是一個(gè)對(duì)象。我們需要設(shè)置自定義報(bào)頭阅悍,跟蹤OAuth訪問令牌好渠,處理client secretsclient ID,處理認(rèn)證或者其它常見的錯(cuò)誤溉箕。為了將這些代碼從App Delegate及我們的模型對(duì)象中分離晦墙,我們將會(huì)把它們統(tǒng)一到GitHubAPIManager中進(jìn)行管理。

在本教程的實(shí)例中我們只與GitHub API打交道肴茄,所以這里只有一個(gè)API管理器晌畅。因此我們?cè)谠擃愔新暶饕粋€(gè)sharedInstance變量,這樣其它調(diào)用者就可以通過它來獲取GitHubAPIManager的唯一實(shí)例:

import Foundation
import Alamofire
import SwiftyJSON
  
class GitHubAPIManager {
  static let sharedInstance = GitHubAPIManager()
} 

接下來我們就可以通過API請(qǐng)求獲取不需認(rèn)證的公共gists列表了寡痰。為了方便我們快速理解抗楔,這里當(dāng)我們獲取API請(qǐng)求結(jié)果后先在控制臺(tái)打印出來。然后我們?cè)侔阉捅砀褚晥D集成拦坠。

因此连躏,我們先聲明這個(gè)簡(jiǎn)單的方法:

class GitHubAPIManager {
  ...
  
  func printPublicGists() -> Void {
    // TODO: 待實(shí)現(xiàn)
  }
}

接下來讓我們創(chuàng)建Router路由,并把新建的文件命名為:GistRouter.swift贞滨。該路由器將負(fù)責(zé)創(chuàng)建URL請(qǐng)求入热,從而能夠讓我們的API管理器保持簡(jiǎn)單拍棕。新建的路由器和前面類似,除了只有一個(gè)獲取公共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)
      }
  }
}

為了可以測(cè)試該代碼勺良,你需要修改MasterViewControllerviewDidAppear方法绰播。該方法在每次主視圖顯示的時(shí)候都會(huì)調(diào)用:

override func viewDidAppear(animated: Bool) { 
  super.viewDidAppear(animated)
  // 開始測(cè)試
  GitHubAPIManager.sharedInstance.printPublicGists() 
  // 結(jié)束測(cè)試
}

保存并運(yùn)行。在模擬器或者你的手機(jī)上你將看到一個(gè)空的表格視圖尚困。但蠢箩,如果API調(diào)用成功,你會(huì)在屏幕的底部(控制臺(tái))看到打印出的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管理器中添加一個(gè)與printPublicGists類似的方法事甜。它將獲取到一個(gè)對(duì)象數(shù)組谬泌,并在控制臺(tái)中打印。

2. 解析API返回的JSON數(shù)據(jù)

API調(diào)用返回了一個(gè)包含gists數(shù)組的JSON對(duì)象逻谦。在API docs for gists中描述了JSON對(duì)象的格式掌实,包含了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對(duì)象轉(zhuǎn)換為Swift對(duì)象跨跨。首先先創(chuàng)建Gist類潮峦,實(shí)體對(duì)象,用來負(fù)責(zé)gists勇婴。在Xcode添加一個(gè)Swift文件,并命名為Gist嘱腥。在這個(gè)文件中我們定義一個(gè)Gist類:

import Foundation

class Gist {
}

查看你的API耕渴,并構(gòu)造一個(gè)你希望在表格視圖中顯示需要的對(duì)象模型類。

現(xiàn)在來看看我們需要從JSON對(duì)象中解析哪些數(shù)據(jù)齿兔。當(dāng)然橱脸,我們也可以解析全部的數(shù)據(jù),但這需要耗費(fèi)很多精力分苇,而且也沒有必要這么做添诉。后面當(dāng)我們需要的時(shí)候,會(huì)從JSON中解析更多的數(shù)據(jù)医寿。

那栏赴,我們需要顯示哪些數(shù)據(jù)呢?表格視圖中的單元格有標(biāo)題靖秩、子標(biāo)題和圖像须眷,因此,我們可以使用gist的描述沟突、作者的GitHub的ID以及作者的頭像來填充花颗。另外,我們還需要每一個(gè)gist的唯一ID和url惠拭。所以扩劝,我們需要為JSON中的每一個(gè)gist解析出這些信息,并創(chuàng)建相應(yīng)的Gist對(duì)象。首先我們?cè)?code>Gist類中添加這些屬性:

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

對(duì)于你的模型對(duì)象棒呛,你需要決定從JSON對(duì)象中解析哪些屬性顯示在表格視圖中聂示。然后像Gist一樣添加相應(yīng)的屬性。

我們希望能夠通過JSON對(duì)象創(chuàng)建一個(gè)Gist實(shí)例条霜,為此我們需要為類增加一個(gè)構(gòu)造函數(shù)催什,該函數(shù)使用JSON作為參數(shù)。這里還需要引入SwiftyJSON庫(kù)宰睡。同時(shí)會(huì)增加一個(gè)簡(jiǎn)單的構(gòu)造函數(shù)蒲凶,這樣我們?cè)跊]有調(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() {
  }
}     

在你的模型對(duì)象類中創(chuàng)建構(gòu)造函數(shù)。如果模型對(duì)象中某些屬性不是字符串拆内,請(qǐng)參考前面的章節(jié)來解析數(shù)字和布爾值旋圆。如果有一些屬性是數(shù)組(如:gist中的文件Files)或者日期,這些屬性的解析我們將在詳細(xì)視圖頁(yè)面進(jìn)行講解麸恍。

3. 創(chuàng)建表格視圖

現(xiàn)在灵巧,我們可以進(jìn)行寫代碼了。前面我們使用Xcode創(chuàng)建了一個(gè)Master-Detail工程抹沪,并默認(rèn)幫我們創(chuàng)建了一些代碼刻肄。下面讓我們快速看一下MasterViewController已經(jīng)為我們做了哪些事情。首先是:

class MasterViewController: UITableViewController { 
  
  var detailViewController: DetailViewController? = nil
  var objects = [AnyObject]()

MasterViewController中有一個(gè)DetailViewController屬性(該屬性是我們?cè)邳c(diǎn)擊視圖中的行時(shí)幫我們導(dǎo)航到詳細(xì)頁(yè)面)融欧,以及一個(gè)對(duì)象數(shù)組敏弃。在這里我們首先對(duì)象數(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)中增加了兩個(gè)按鈕:左邊增加一個(gè)編輯按鈕麦到,右邊增加一個(gè)新建按鈕。

通過detailViewController屬性欠肾,我們就可以在詳情頁(yè)面中顯示用戶所選中g(shù)ist的詳細(xì)信息瓶颠。

然后:

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

在視圖顯示之前,我們需要調(diào)用一下clearsSelectionOnViewWillAppear刺桃,這樣就可以在我們打開其它頁(yè)面時(shí)仍然保持行的選中狀態(tài)粹淋。這個(gè)在iPad的分屏視圖中有用,iPhone由于僅使用表格視圖虏肾,所以該方法沒有意義廓啊。

在視圖顯示的時(shí)候我們需要從GitHub中加載數(shù)據(jù)。因此封豪,可以在viewDidAppear方法中來實(shí)現(xiàn):

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

通常谴轮,我們應(yīng)當(dāng)在viewWillAppear中來加載數(shù)據(jù),這樣視圖就可以很快的顯示吹埠。因?yàn)楹竺嫖覀冃枰獧z查用戶是否已經(jīng)登錄第步,如果沒有疮装,那么會(huì)彈出一個(gè)登錄視圖讓用戶登錄,但是粘都,如果當(dāng)前視圖沒有顯示完畢廓推,是無(wú)法加載另外一個(gè)視圖的。因此翩隧,這里我們使用viewDidAppear樊展。

創(chuàng)建一個(gè)類似loadGists的方法來加載你的數(shù)據(jù)。

后面我們會(huì)重構(gòu)loadGists中的代碼堆生,這樣就可以得到Gist的數(shù)組专缠,并顯示到視圖中。

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

假如我們有一些很重的資源文件(如:大的圖片)或者一些可重建的對(duì)象淑仆,那么我們就可以在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)
}

新建按鈕將會(huì)調(diào)用insertNewObject方法蔗怠。該方法將創(chuàng)建一個(gè)新的對(duì)象墩弯,并把它添加到表格視圖中。這個(gè)功能我們要后面很久才會(huì)實(shí)現(xiàn)寞射,因此這里先彈出一個(gè)對(duì)話框告訴大家還沒有實(shí)現(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方法渔工,該方法將會(huì)跳轉(zhuǎn)到詳情頁(yè)面:

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

這里,我們還是要把通用的對(duì)象替換為我們的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
      }
    }
  }
}

后面我們會(huì)設(shè)置詳情視圖中所要顯示的gists。

接下來的幾個(gè)方法是告訴表格視圖如何進(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
}

再一次策治,我們這里需要將對(duì)象轉(zhuǎn)換為gists,并且把tableView:cellForRowAIndexPath:indexPath:更改為顯示gists的描述和擁有者的ID兰吟。后面再來實(shí)現(xiàn)如何顯示擁有者的頭像通惫,因?yàn)轱@示圖像需要額外一些處理,這里我們不想因?yàn)檫@個(gè)而停下來混蔼。

首先履腋,調(diào)整故事板中的表格視圖單元格,因?yàn)槲覀冃枰谏厦骘@示兩行文本:

  1. 打開mainStoryboard并選中masterViewController中的Table View
  2. 選擇表格視圖中單元格原型并將類型('Style')屬性更改為Subtitle惭嚣,這樣我們就會(huì)有兩個(gè)文本了

<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)建∽窈現(xiàn)在我們簡(jiǎ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)行晚吞,但是你會(huì)發(fā)現(xiàn)顯示的仍然是一個(gè)空白表格視圖延旧。為了測(cè)試我們可以構(gòu)建一些假的本地?cái)?shù)據(jù)而不是從GitHub上請(qǐng)求。修改loadGists()方法槽地,在方法中創(chuàng)建一個(gè)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)擊增加按鈕時(shí)芦瘾,會(huì)彈出一個(gè)提示框:

<div style="text-align: center">
<img src="http://o6pjjbgcb.bkt.clouddn.com/rest_050.png?imageView2/0/w/400" style="width:320px"/>
</div>

像上面一樣確保你的對(duì)象可以顯示在表格視圖。

現(xiàn)在表格視圖功能應(yīng)該是沒有問題了集畅,那么下面我們恢復(fù)loadGists()函數(shù):

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

4. 獲取并解析API的響應(yīng)

回想一下近弟,我們?cè)谇懊鎰?chuàng)建的Alamofire.Request的擴(kuò)展:

public func responseObject<T: ResponseJSONObjectSerializable>

這個(gè)擴(kuò)展用來處理Alamofire的響應(yīng),并將返回來的JSON格式數(shù)據(jù)轉(zhuǎn)換為Swift對(duì)象(當(dāng)然挺智,相應(yīng)類需要實(shí)現(xiàn)ResponseJSONObjectSerializable協(xié)議中的初始化方法)〉挥洌現(xiàn)在我們需要實(shí)現(xiàn)的與這個(gè)很類似,只不過需要將返回的JSON數(shù)組轉(zhuǎn)換為Swift對(duì)象數(shù)組赦颇。因此二鳄,我們保留這個(gè)協(xié)議,并將它添加到工程中沐扳。創(chuàng)建一個(gè)ResponseJSONObjectSerializable.swift文件泥从,并把協(xié)議定義添加進(jìn)去。在文件中別忘了引入SwiftyJSON庫(kù):

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

然后修改Gist類沪摄,實(shí)現(xiàn)該協(xié)議(注意躯嫉,我們前面已經(jīng)實(shí)現(xiàn)了相應(yīng)的構(gòu)造方法):

class Gist: ResponseJSONObjectSerializable { 
  ...
}

我們也把responseObject函數(shù)拷貝進(jìn)來,因?yàn)楹竺鏁?huì)使用到它杨拐。創(chuàng)建AlamofireRequest+JSONSerializable.swift文件祈餐,因?yàn)椋?code>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 = "無(wú)法進(jìn)行對(duì)象序列化帆阳,因?yà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 = "無(wú)法通過JSON創(chuàng)建對(duì)象" 
        let error = Error.errorWithCode(.JSONSerializationFailed,
          failureReason: failureReason)
        return .Failure(error) 
      }
    case .Failure(let error): 
      return .Failure(error)
    }
  }
  return response(responseSerializer: serializer, completionHandler: completionHandler) 
}

我們的需求和這個(gè)類似屋吨,只不過返回的是[T]對(duì)象數(shù)組蜒谤,而不是一個(gè)[T]對(duì)象:

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

具體實(shí)現(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 = "無(wú)法解析為數(shù)組,因?yà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) 
      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中的每個(gè)元素for (_, item) in json鳍徽,并為它創(chuàng)建相應(yīng)的對(duì)象:let object = T(json: item)。如果對(duì)象創(chuàng)建成功則把它添加到數(shù)組中敢课。

現(xiàn)在阶祭,我們需要:

  1. 完成我們的函數(shù)使其獲取公共的gists,并把返回值解析為一個(gè)數(shù)組
  2. 將函數(shù)更改為返回gists數(shù)組并傳給表格視圖

getPublicGists函數(shù)看起來很像之前的printPulicGists

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

最大的不同就是將打印替換為返回一個(gè)數(shù)組直秆。因此濒募,我們把responseString替換為responseArray。現(xiàn)在我們可以把這個(gè)函數(shù)加入到GitHubAPIManager中了:

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

這看起來有點(diǎn)奇怪圾结。我們前面不是說要返回一個(gè)數(shù)組么瑰剃,但這里返回的是Void啊。嗯疫稿,是的培他,這是因?yàn)锳PI的調(diào)用是異步的鹃两,我們發(fā)起一個(gè)請(qǐng)求,然后當(dāng)請(qǐng)求處理完畢后我們會(huì)收到一個(gè)通知舀凛。我們可以將這個(gè)處理放在完成處理程序中俊扳。下面我們來添加一個(gè)塊代碼,這樣當(dāng)請(qǐng)求處理完畢后就可以調(diào)用了猛遍。我們的完成處理程序需要處理兩種可能:一種情況是正確返回了一個(gè)Gists數(shù)組馋记,另外一種情況就是返回了一個(gè)錯(cuò)誤。

完成處理程序的簽名是(Result<T>, NSError)懊烤。它是Alamofire所創(chuàng)建的一個(gè)指定對(duì)象梯醒,這樣可以讓我們?cè)?code>.Success情況下返回一個(gè)Gists數(shù)組,在.Failure情況下返回一個(gè)錯(cuò)誤腌紧。

因此我們發(fā)送請(qǐng)求后茸习,將響應(yīng)序列化器(response serializer)設(shè)置為我們上面所創(chuàng)建的responseArray。然后在調(diào)用成功后返回Gists數(shù)組壁肋,或者失敗時(shí)返回一個(gè)錯(cuò)誤:

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

因?yàn)?code>responseArray的完成處理程序返回的數(shù)組參數(shù)中是一個(gè)泛型對(duì)象号胚,因此我們需要修改讓它明確返回的對(duì)象類型,因此將參數(shù)修改為:Respoonse<[Gist], NSError>浸遗。否則當(dāng)數(shù)據(jù)返回時(shí)將不知道如何創(chuàng)建相應(yīng)的對(duì)象猫胁。

getPublicGists的完成處理程序匹配responseArray中的一個(gè),并不需要在這里處理任何錯(cuò)誤。因此,在.responseArray的塊(block)中只需要調(diào)用完成處理程序即可惭婿。對(duì)于錯(cuò)誤的處理則是調(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)建一個(gè)函數(shù)像getPublicGists一樣,返回你的業(yè)務(wù)對(duì)象數(shù)組渊啰。

Ok,現(xiàn)在讓我們看看我們需要在什么時(shí)候調(diào)用getPublicGists。先看看之前在MasterViewController是在如何調(diào)用的:

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

看來最好是將printPublicGists()替換為getPublicGist。這個(gè)非常容易做:

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ā)起一個(gè)異步調(diào)用锈至,并返回gists數(shù)組。如果成功調(diào)用译秦,我們會(huì)把gists保存到本地?cái)?shù)組變量中,并告訴表格視圖使用新的數(shù)據(jù)刷新顯示击碗。簡(jiǎn)單漂亮筑悴!

創(chuàng)建你的loadGists方法,并調(diào)用之前你的getPublicGists方法稍途,把返回的結(jié)果保存到數(shù)組對(duì)象中阁吝,這樣你的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)滾動(dòng)時(shí)加載更多Gists
  • 下拉刷新
  • Gists的詳細(xì)視圖
  • 刪除Gists
  • 新建Gists

GitHub上本章的代碼:tableview

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末甲馋,一起剝皮案震驚了整個(gè)濱河市埂奈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌定躏,老刑警劉巖账磺,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異痊远,居然都是意外死亡垮抗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門碧聪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冒版,“玉大人,你說我怎么就攤上這事逞姿〈俏耍” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵哼凯,是天一觀的道長(zhǎng)欲间。 經(jīng)常有香客問我,道長(zhǎng)断部,這世上最難降的妖魔是什么猎贴? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蝴光,結(jié)果婚禮上她渴,老公的妹妹穿的比我還像新娘。我一直安慰自己蔑祟,他們只是感情好趁耗,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著疆虚,像睡著了一般苛败。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上径簿,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天罢屈,我揣著相機(jī)與錄音,去河邊找鬼篇亭。 笑死缠捌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的译蒂。 我是一名探鬼主播曼月,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼谊却,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了哑芹?” 一聲冷哼從身側(cè)響起炎辨,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绩衷,沒想到半個(gè)月后蹦魔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡咳燕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年勿决,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片招盲。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡低缩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出曹货,到底是詐尸還是另有隱情咆繁,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布顶籽,位于F島的核電站玩般,受9級(jí)特大地震影響礼饱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜镊绪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蝴韭。 院中可真熱鬧,春花似錦榄鉴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)减余。三九已至位岔,卻和暖如春如筛,著一層夾襖步出監(jiān)牢的瞬間抒抬,已是汗流浹背杨刨。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留擦剑,地道東北人妖胀。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像惠勒,于是被迫代替她去往敵國(guó)和親赚抡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容