1. 程式人生 > >(Swift) iOS Apps with REST APIs(五) -- 整合REST API和表格檢視

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

本文將繼續前面的教程,繼續講解如何通過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選項。

然後,開啟型別為.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 secretsclient 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。後面再來實現如何顯示擁有者的頭像,因為顯示影象需要額外一些處理,這裡我們不想因為這個而停下來。 首先,調整故事板中的表格檢視單元格,因為我們需要在上面顯示兩行文字: 1. 開啟`mainStoryboard`並選中`masterViewController`中的`Table View` 2. 選擇表格檢視中單元格原型並將型別(‘Style’)屬性更改為`Subtitle`,這樣我們就會有兩個文字了

接下來就可以修改程式碼來顯示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介面如下:

當你點選增加按鈕時,會彈出一個提示框:

像上面一樣確保你的物件可以顯示在表格檢視。

現在表格檢視功能應該是沒有問題了,那麼下面我們恢復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)`。如果物件建立成功則把它新增到陣列中。 現在,我們需要: 1. 完成我們的函式使其獲取公共的gists,並把返回值解析為一個數組 2. 將函式更改為返回gists陣列並傳給表格檢視 `getPublicGists`函式看起來很像之前的`printPulicGists`:
func printPublicGists() -> Void { 
  Alamofire.request(GistRouter.GetPublic())
    .responseString { response in
      if let receivedString = response.result.value {
        print(receivedString)
      }
    }
}
最大的不同就是將列印替換為返回一個數組。因此,我們把`responseString`替換為`responseArray`。現在我們可以把這個函式加入到`GitHubAPIManager`中了:
func getPublicGists() -> Void { 
  Alamofire.request(GistRouter.GetPublic())
    .responseArray {
      ...
  }
}
這看起來有點奇怪。我們前面不是說要返回一個數組麼,但這裡返回的是`Void`啊。嗯,是的,這是因為API的呼叫是非同步的,我們發起一個請求,然後當請求處理完畢後我們會收到一個通知。我們可以將這個處理放在完成處理程式中。下面我們來新增一個塊程式碼,這樣當請求處理完畢後就可以呼叫了。我們的完成處理程式需要處理兩種可能:一種情況是正確返回了一個Gists陣列,另外一種情況就是返回了一個錯誤。 完成處理程式的簽名是`(Result, NSError)`。它是Alamofire所建立的一個指定物件,這樣可以讓我們在`.Success`情況下返回一個Gists陣列,在`.Failure`情況下返回一個錯誤。 因此我們傳送請求後,將響應序列化器(response serializer)設定為我們上面所建立的`responseArray`。然後在呼叫成功後返回Gists陣列,或者失敗時返回一個錯誤:
func getPublicGists(completionHandler: (Result<[Gist], NSError>) -> Void) {
  Alamofire.request(.GET, "https://api.github.com/gists/public")
    .responseArray { (response:Response<[Gist], NSError>) in 
      ...
  }
}
因為`responseArray`的完成處理程式返回的陣列引數中是一個泛型物件,因此我們需要修改讓它明確返回的物件型別,因此將引數修改為:`Respoonse
func getPublicGists(completionHandler: (Result<[Gist], NSError>) -> Void){ 
  Alamofire.request(GistRouter.GetPublic())
    .responseArray { (response:Response<[Gist], NSError>) in 
      completionHandler(response.result)
  } 
}

建立一個函式像getPublicGists一樣,返回你的業務物件陣列。

Ok,現在讓我們看看我們需要在什麼時候呼叫getPublicGists。先看看之前在MasterViewController是在如何呼叫的:

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

override func viewDidAppear(animated: Bool) { 
  super.viewDidAppear(animated)
  loadGists()
}
看來最好是將`printPublicGists()`替換為`getPublicGist`。這個非常容易做:
func loadGists() { 
  GitHubAPIManager.sharedInstance.getPublicGists() { result in
    guard result.error == nil else { 
      print(result.error)
      // TODO: display error
      return
    }

    if let fetchedGists = result.value { 
      self.gists = fetchedGists  
    }
    self.tableView.reloadData()
  }
}
我們發起一個非同步呼叫,並返回gists陣列。如果成功呼叫,我們會把gists儲存到本地陣列變數中,並告訴表格檢視使用新的資料重新整理顯示。簡單漂亮! > 建立你的`loadGists`方法,並呼叫之前你的`getPublicGists`方法,把返回的結果儲存到陣列物件中,這樣你的`MasterViewConroller`就可以將它們顯示到表格檢視中了。 現在API呼叫和表格檢視已經很好的整合了。儲存並執行看看效果。

小結

到這裡我們已經完成了app的核心功能。下面我們逐步新增以下功能:

  • 在單元格中顯示圖片
  • 當滾動時載入更多Gists
  • 下拉重新整理
  • Gists的詳細檢視
  • 刪除Gists
  • 新建Gists

GitHub上本章的程式碼:tableview