(Swift)iOS Apps with REST APIs(十二) -- 視圖控制器切換及數組、日期格式的JSON解析

本篇主要講解如何從列表視圖轉換到詳細視圖,及如何使用SFSafariViewController來顯示一個Web視圖。此外,還講解了如何對JSON中的數組及日期進行解析。

重要說明: 這是一個系列教程,非本人原創,而是翻譯國外的一個教程。本人也在學習Swift,看到這個教程對開發一個實際的APP非常有幫助,所以翻譯共享給大家。原教程非常長,我會陸續翻譯并發布,歡迎交流與分享。

到目前為止我們的Swift應用具有的功能有:

  • 使用Alamofire框架通過GitHub Gists API獲取數據
  • 使用自定義響應序列化將JSON數據解析為Gist對象數組
  • 將結果顯示在表格視圖中,并通過URL加載圖片
  • 可以讓用戶選擇3種不同的列表
  • 滾動加載更多
  • 下拉刷新

在本章中我們將添加更多“實際”可用的功能,這些功能包含:

  • 從JSON中解析對象數組(這里需要解析的是文件列表)以及日期解析(使用NSDateFormatter)
  • 表格視圖與詳情視圖之間的數據傳遞,當用戶在列表中點擊一行時將在詳情視圖中打開該Gist,并顯示更詳細的數據
  • 使用代碼創建一個新的視圖控制器,用來顯示Gist中的文件內容。當用戶點擊文件名稱時將在一個web視圖中顯示該文件的內容

后面兩個功能使用了不同的方式進行視圖控制器的切換。到詳細視圖的轉換使用了segue,到文件內容視圖的轉換則使用了導航控制器將一個新的視圖控制器壓入棧中。

JSON解析:數組和日期

當我們的APP啟動時,就會通過API調用獲取一個JSON格式的Gist列表,然后通過我們自定義的響應序列化將JSON數據轉換成Gist對象。到目前為止,我們僅僅解析了JSON數據中的字符串,接下來我們將解析JSON數據中的文件數組及Gist的創建日期和最后更新日期。

首先在Gist類中增加幾個屬性:

class Gist: ResponseJSONObjectSerializable { 
  var id: String?
  var description: String?
  var ownerLogin: String?
  var ownerAvatarURL: String? 
  var url: String?
  var files:[File]?
  var createdAt:NSDate?
  var updatedAt:NSDate?
  ...
}

我們還需要一個來負責Gist中的文件信息(一個Gist可能包含多個文件)的類。對于Gist中的文件我們只需要文件名稱以及文件具體內容的URL,然后我們可以在web視圖中顯示具體的文件內容。下面讓我們創建一個File.swift文件,并實現File類:

import SwiftyJSON

class File: ResponseJSONObjectSerializable { 
  var filename: String?
  var raw_url: String?
}

Gist一樣,File類需要實現一個從JSON數據創建對象的方法,因此需要實現ResponseJSONObjectSerializable協議:

class File: ResponseJSONObjectSerializable { 
  var filename: String?
  var raw_url: String?

  required init?(json: JSON) { 
    self.filename = json["filename"].string 
    self.raw_url = json["raw_url"].string
  }
}

然后擴展Gists的初始化方法對文件進行解析(我們后面再對日期進行處理):

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
  
  // files
  self.files = [File]()
  if let filesJSON = json["files"].dictionary {
    for (_, fileJSON) in filesJSON {
      if let newFile = File(json: fileJSON) {
        self.files?.append(newFile) 
      }
    }
  }
  
  // TODO: dates
}

對于Gist中的文件,我們創建一個數組:self.files = [File]()

在JSON數據中文件列表格式為:

"files": {
  "ring.erl": {
    "size": 932,
    "raw_url": "https://gist.githubusercontent.com/raw/365370/8c4d2d43d178df44f4c03a7f2ac0\ff512853564e/ring.erl",
    "type": "text/plain", 
    "language": "Erlang", 
    "truncated": false, 
    "content": "content of gist"
  },
  ...
}

我們將循環每一個文件解析出filenameraw_url(譯者注:這里給出的JSON格式中并沒有filename屬性,GitHub的API文檔中也沒有,但實際返回的數據中是有filename屬性的):

if let filesJSON = json["files"].dictionary { 
  for (_, fileJSON) in filesJSON {
    if let newFile = File(json: fileJSON) { 
      self.files?.append(newFile)
    }
  }
}

因為filesJSON是一個字典類型(dictionary),所以我們可以使用for(_, fileJSON) in filesJSON循環每一個文件。這里_是一個字典key的占位符,表示我們不會使用該值。然后,我們將嘗試使用JSON數據創建一個File對象,如果創建成功,將它添加到文件數組中:

if let newFile = File(json: fileJSON) { 
  self.files?.append(newFile)
}

以上就是我們從JSON中解析文件數組的代碼。

日期解析

對于日期改如何進行解析呢?因為我們從服務器中獲得的日期是字符串格式的,我們希望能夠把它轉換為NSDate對象,這里我們會使用到NSDateFormatterNSDateFormatter可以將字符串轉換為NSDate,也可以將NSDate轉換為字符串。

服務器返回的日期格式為:2014-12-10T16:44:31.486000Z。為了可以解析該字符串我們需要設置一些參數:

class func dateFormatter() -> NSDateFormatter {
  let aDateFormatter = NSDateFormatter()
  aDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
  aDateFormatter.timeZone = NSTimeZone(abbreviation: "UTC")
  aDateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
  return aDateFormatter
}

當使用NSDateFormatters對服務器返回的時間進行格式化時,localetimeZone是非常有用的,因為我們的用戶可能使用不是同一種語言,位于不同的時區,使用不同的日期格式。但當將時間顯示給用戶看時,應該使用NSDateFormatterShortStyle這樣用戶將看到更符合他們習慣日期格式。

現在我們就可以使用日期格式化對創建時間和更新時間進行解析:

class Gist { 
  ...

  required init?(json: JSON) { 
    ...

    // Dates
    let dateFormatter = Gist.dateFormatter()
    if let dateString = json["created_at"].string {
      self.createdAt = dateFormatter.dateFromString(dateString) 
    }
    if let dateString = json["updated_at"].string { 
      self.updatedAt = dateFormatter.dateFromString(dateString)
    }
  }
  
  ...
}

創建一個NSDateFormatter及改變它的屬性代價都是非常高昂的。這段代碼中每次進行Gist解析時都會創建一個新的NSDateFormatter,因此這里我將對它進行優化,讓所有的解析都使用一個NSDateFormatter實例。代碼如下:

static let sharedDateFormatter = Gist.dateFormatter() 
required init?(json: JSON) {
  ...
  
  let dateFormatter = Gist.sharedDateFormatter 
  if let dateString = json["created_at"].string {
    self.createdAt = dateFormatter.dateFromString(dateString) 
  }
  if let dateString = json["updated_at"].string { 
    self.updatedAt = dateFormatter.dateFromString(dateString)
  }
}

現在我們運行,它看起來也沒有什么不同。不用擔心,獲取并顯示數據并沒有多少困難。下面讓我們來完善一開始創建項目時就創建的詳細視圖控制器。處理的流程大致如下,當我們在列表視圖中點擊了一個Gist時,將轉換到詳細視圖并顯示該Gist的詳細信息。

回頭檢查一下業務對象的JSON數據。并從中間選取幾個屬性用來在詳細視圖中顯示,這些屬性可能是日期或者一些數組。將它們添加到實體類中并解析。

配置詳細視圖控制器

打開DetailViewController文件,它看起來如下:

import UIKit
class DetailViewController: UIViewController {
  
  @IBOutlet weak var detailDescriptionLabel: UILabel!
  
  var detailItem: AnyObject? { 
    didSet {
      // Update the view.
      self.configureView() 
    }
  }
  
  func configureView() {
    // Update the user interface for the detail item. 
    if let detail = self.detailItem {
      if let label = self.detailDescriptionLabel { 
        label.text = detail.description
      }
    }
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib. 
    self.configureView()
  }
  
  override func didReceiveMemoryWarning() { 
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
  }
}

在故事板中詳細頁面視圖也是非常簡單的,它緊緊顯示了一個標簽,該視圖我們將用來顯示詳細信息。

我們接下來將完善該UI,但是首先我們先將詳細對象的類型由通用的對象AnyObject更改為我們所要顯示的Gist。同時我們還需要將修改變量的名稱,修改后的代碼如下:

var gist: Gist? { 
  didSet {
    // Update the view.
    self.configureView()
  }
}
func configureView() {
  // Update the user interface for the detail item. 
  if let currentGist = self.gist {
    if let label = self.detailDescriptionLabel { 
      label.text = currentGist.description
    }
  }
}

那,該視圖是從哪里獲取Gist的呢?我們必須找出來,并對這些變量進行改名。這里有兩種方式可以達到我們的目的:

  1. 在整個項目中查找"detailItem"
  2. 嘗試編譯項目,那么在使用detailItem的地方編譯器就會報錯

我們不應該逐個去找detailItem。在Objective-C中我們可以通過重構工具直接對變量進行重命名。但swift中還沒有提供該方法。希望該工具可以早點實現。

通過上面的方法,我們在詳細視圖控制器之外找到了2處使用的地方。下面我們需要使用gist替換這兩個地方。

AppDelegate中:

func splitViewController(splitViewController: UISplitViewController, 
  collapseSecondaryViewController secondaryViewController:UIViewController,  
  ontoPrimaryViewController primaryViewController:UIViewController) -> Bool {
    guard let secondaryAsNavController = secondaryViewController as? 
      UINavigationController else { return false }
    guard let topAsDetailController = secondaryAsNavController.topViewController 
      as? DetailViewController else { return false }
    if topAsDetailController.gist == nil {
      // Return true to indicate that we have handled the collapse by doing nothing 
      // the secondary controller will be discarded.
      return true
    }
    return false
}

另外一個是在MasterViewController中,表格視圖將選中的gist傳遞給DetailViewController進行顯示。這里我們還要將類型由NSData修改為Gist就像我們修改名稱一樣。我們還要檢查導航控制器中的頂層視圖控制器是否是DetailViewController,而不是假定它是,以防萬一我們曾經改變了導航:

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.gist = gist 
        detailViewController.navigationItem.leftBarButtonItem =
          self.splitViewController?.displayModeButtonItem() 
        detailViewController.navigationItem.leftItemsSupplementBackButton = true
      }
    }
  }
}

更改你的DetailViewController中的實體對象模型的類,并修改其它地方對它的引用。

現在我們可以編譯并運行了。當我們在列表視圖中點擊了一個Gist后,詳細視圖中將會顯示它的描述:

在Segue中傳遞數據

但這是怎么工作的呢?在故事板中我們可以看到在MasterViewController的表格視圖的單元格中有一個到DetailViewController的連接(實際上是導航控制器持有DetailViewController,但那只是讓我們又一個更好的標題欄而已):

可以使用Editor-Canvas->Zoom進行放大。Segue就在那里。

因此,當在表格視圖中點擊時Segue就會被激活。當從表格視圖轉換(也就是"segue")到詳細視圖時就會調用prepareForSeque方法。

prepareForSegue中:

  • 獲取我們將要轉換到的視圖控制器: let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
  • 通過indexPathForSelectedRow()獲取所點擊的行。當我們表格視圖中有多個區段時,indexPath可以獲取到選中的行及區段信息
  • 獲取選中的Gist: let object = gists[indexPath.row]
  • 將它傳遞給目標視圖控制器: controller.gist = object

這就是Xcode為我們構建的Master-Detail項目。我使用我們所創建的Gist來更新:

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

添加一個表視圖

接下來讓我們通過使用表視圖顯示數據來完善視圖顯示。首先,我們讓DetailViewController實現UITableViewDataSourceUITableViewDeletate。這里我們是可以這么做的,即使DetailViewController不是一個UITableViewController。當表視圖只是視圖的一部分時這種方式就非常有用,比如你希望在表視圖上面有一個頭,它并不隨著表視圖的滾動而滾動。另外,我們還需要增加一個對表視圖的引用,并刪除原來那個標簽控件:

class DetailViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 
  @IBOutlet weak var tableView: UITableView!
  ...

下面讓我們轉到主故事板,這樣我們就可以添加表視圖了。打開Main.Storyboard并選擇DetailViewController

在詳細視圖控制器中,先刪除標簽,然后將表視圖拖拽到控制器中,并讓它充滿整個視圖:

然后向表視圖中添加一個原型單元格。然后把identifier設置為"Cell"。

在右上方的面版中選擇connection organizer(最后一個頁簽,圓形中間有一個小箭頭)。將詳細視圖控制器連接成為表視圖的數據源(data source)和代理(delegate)。然后將詳細視圖中的tableView IBOutlet鏈接到表視圖:

現在我們就可以轉會到詳細視圖控制器,為表視圖填充數據并顯示。這里我們有2個區段:一個用來顯示gist的基礎數據,如描述等;另外一個用來顯示gist中的文件列表。然后當點擊文件時,顯示它們的詳細內容。

因為我們有兩個區段,我們還需要為它們設置相應的標題。第一個區段有兩個項目需要顯示(描述信息和擁有者信息),第二個區段需要顯示所包含文件的列表:

func numberOfSectionsInTableView(tableView: UITableView) -> Int { 
  return 2
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 
  if section == 0 {
    return 2 
  } else {
    return gist?.files?.count ?? 0 
  }
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 
  if section == 0 {
    return "About" 
  } else {
    return "Files" 
  }
}

??是空和運算符。它表示,如果不為空就使用該值,否則就使用默認值。因此return gist?.files?.count ?? 0將返回文件的個數,如果gist中沒有文件就返回0.

這樣我們就可以使用這些數據來填充表視圖的單元格了:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
  
  if indexPath.section == 0 { 
    if indexPath.row == 0 {
      cell.textLabel?.text = gist?.description 
    } else if indexPath.row == 1 {
      cell.textLabel?.text = gist?.ownerLogin
    }
  } else {
    if let file = gist?.files?[indexPath.row] {
      cell.textLabel?.text = file.filename
    }
  }
  return cell 
}

當gist賦值后我們還得告訴表視圖重新進行加載:

func configureView() {
  // Update the user interface for the detail item. 
  if let detailsView = self.tableView {
    detailsView.reloadData()
  }
}

現在就可以編譯運行了。

構建你的表視圖或者使用IBOutlet自定義詳細視圖控制器中的視圖,展示你的詳細信息。

雖然我們可以看到文件的列表了,但是它還不是很有用。下面讓我們來增加一些有用功能:

  1. 當點擊文件名時顯示文件的具體內容
  2. 可讓用戶收藏/取消收藏該gist

顯示文件內容

我們在解析JSON數據的時候,并沒有獲取到文件的具體內容,而是獲取到這些文件的URL路徑。我們可以使用web視圖來顯示這個URL。當然還有更好的方式,但現在使用web視圖就足夠了。

在處理OAuth的時候我們已經引入了Safari Services framework。如果你還沒有做,那么你需要把它添加到工程中。

DetailViewController中引入SafariServices框架:

import UIKit
import SafariServices
class DetailViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 
...
}

當用戶點擊一個文件名時:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 
  if indexPath.section == 1 {
    if let file = gist?.files?[indexPath.row], 
      urlString = file.raw_url,
      url = NSURL(string: urlString) {
        let safariViewController = SFSafariViewController(URL: url) 
        safariViewController.title = file.filename 
        self.navigationController?.pushViewController(safariViewController, animated: true)
    }
  }
}

當用戶點擊文件名時,我們可以獲取到相應的URL:

if let file = gist?.files?[indexPath.row], 
  urlString = file.raw_url,
  url = NSURL(string: urlString) {

然后我們就創建一個Safari視圖控制器,并把URL傳遞給它顯示,在導航欄中顯示文件名稱:

let safariViewController = SFSafariViewController(URL: url) safariViewController.title = file.filename

最后使用導航控制器切換到該視圖:

self.navigationController?.pushViewController(safariViewController, animated: true)

使用導航控制器就意味著在新的視圖中顯示一個退回按鈕,并且允許使用滑動手勢返回。這樣我們也就不用增加新的按鈕來處理這些功能了。

看看你的工程中是否有那些可以使用web視圖進行顯示的。如果有,就按照上面的步驟實現它。

小結

接下來我們將為gist添加收藏和取消收藏功能。

本章的代碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容

  • 本文將繼續前面的教程,繼續講解如何通過REST API獲取數據列表并解析為Swift對象,然后顯示在表格視圖中。 ...
    CD826閱讀 1,129評論 0 6
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,814評論 18 139
  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一種新的協議。它實...
    香橙柚子閱讀 24,008評論 8 183
  • 安徽安慶大觀十里中心學校 汪瓊 題記: 當你用祈禱使一天終結, 親愛的,你那一雙小手, 我想,也該用來為那些 丟失...
    犟龜汪瓊閱讀 183評論 0 1
  • 愛情是一朵帶刺的玫瑰—是我見過對愛情最簡短最精煉的解讀。 我們總是被玫瑰嬌艷紅火的外表和迷人的香氣給吸引,一度沉淪...
    兩顆星blingbling閱讀 215評論 1 0