本文是Intermediate iOS 11 Programming with Swift 4系列?的 第 四 篇.

JSON是什么?
JSON (JavaScript對象表示法的簡稱)是一種基于文本的、輕量級的、易于存儲和交換數(shù)據(jù)的方法。它通常用于表示客戶機-服務器應用程序中的結構化數(shù)據(jù)和數(shù)據(jù)交換,作為XML的替代品。我們每天使用的許多web服務都有基于json的api。包括Twitter、Facebook和Flickr在內(nèi)的大多數(shù)iOS應用程序都以JSON格式將數(shù)據(jù)發(fā)送到后端web服務。
例如:?

如您所見,JSON格式的數(shù)據(jù)比XML更易于閱讀和解析。我不會詳細介紹JSON。這不是本章的目的。如果您想了解更多關于該技術的信息,我建議您訪問json官網(wǎng)的JSON指南。
自從ios5發(fā)布以來,iOS SDK已經(jīng)讓開發(fā)者可以輕松地獲取和解析JSON數(shù)據(jù)。它附帶了一個名為NSJSONSerialization的方便類,可以自動將JSON格式的數(shù)據(jù)轉換為對象。在本章的后面,我將向您展示如何使用API來解析web服務返回的一些JSON格式的示例數(shù)據(jù)。一旦您了解了它的工作原理,就可以通過與其他免費/付費web服務集成來構建一個應用程序。
在Swift 4(或iOS11)中,蘋果引入了可編碼協(xié)議來簡化整個JSON歸檔和序列化過程。我們還將研究這個新特性,看看如何在JSON解析中應用它。
Demo App
“像往常一樣,我們會創(chuàng)建一個演示應用程序。我們叫它KivaLoan吧?!蔽覀冎悦鸎ivaLoan應用,是因為我們將利用kiva.org提供的基于jsf的API。
如果你還沒聽說過Kiva,它是一個非營利組織,它的使命是“通過貸款幫助人們減輕貧困”。它允許個人貸出至多25美元,以幫助在世界各地創(chuàng)造機會。Kiva為開發(fā)者提供免費的基于web的api來訪問他們的數(shù)據(jù)。對于我們的演示應用程序,我們將調(diào)用以下Kiva API來檢索最近的籌款貸款,并將它們顯示在表視圖中:
https://api.kivaws.org/v1/loans/newest.json
小提示:從ios9開始,蘋果推出了一項名為應用程序傳輸安全(ATS)的功能,目的是提高應用程序和web服務之間連接的安全性。默認情況下,所有出站連接都應該使用HTTPS。否則,您的應用程序?qū)⒉辉试S連接到web服務。您可以選擇在Info中添加一個名為nsallowsarbitraryload的鍵。plist并將值設置為YES以禁用ATS,以便您可以通過HTTP連接到web api. ?然而,如果你在你的應用程序中使用nsallowsarbitraryload的話,你必須注意這一點。在ios10系統(tǒng)中,蘋果進一步加強了所有iOS應用的ATS系統(tǒng)。到2017年1月,所有iOS應用程序都應該兼容ats。換句話說,如果您的應用程序連接到任何外部web服務,那么連接必須通過HTTPS。如果你的應用不能滿足這個要求,蘋果將不會允許它在應用商店發(fā)布。
上述API的返回數(shù)據(jù)為JSON格式。下面是一個示例結果:

為了讓您專注于學習JSON實現(xiàn),您可以首先從這里下載項目模板。我已經(jīng)為你創(chuàng)建了這個應用的框架。它是一個簡單的基于表格的應用程序,它顯示了Kiva.org提供的貸款列表。項目模板包括為表視圖控制器和原型單元格預先構建的故事板和自定義類。如果你運行模板,它會產(chǎn)生一個空的表格應用。

創(chuàng)建JSON數(shù)據(jù)模型
我們將首先創(chuàng)建一個類來建模貸款。加載JSON不是必需的,但是最佳實踐是創(chuàng)建一個單獨的類(或結構)來存儲數(shù)據(jù)模型。貸款類表示KivaLoan應用程序中的貸款信息,用于存儲Kiva.org返回的貸款信息。為了簡單起見,我們不會使用所有返回的貸款數(shù)據(jù)。相反,應用程序?qū)⒅伙@示以下貸款字段:
貸款申請人的名字:
name = " Mar\U00e8me "
貸款申請人的國家:?

貸款做什么:
use = " to buy fabric resell " ;
貸款金額:
" loan_amount" = 750;
這些字段足以填充表格視圖中的標簽?!爆F(xiàn)在使用Swift文件模板創(chuàng)建一個新的類文件。命名為貸款。swift并聲明貸款結構如下:
struct Loan {
? ? var name: String = ""
? ? var country: String = ""
? ? var use: String = ""
? ? var amount: Int = 0
}
JSON支持一些基本數(shù)據(jù)類型,包括數(shù)字、字符串、布爾值、數(shù)組和對象(鍵和值對的關聯(lián)數(shù)組)。對于貸款字段,貸款金額作為數(shù)字值存儲在json格式的數(shù)據(jù)中。這就是為什么我們用Int類型聲明amount屬性。對于其他字段,它們是用String類型聲明的。
用Kiva API取回貸款
Kiva API是免費使用的。不需要注冊。點擊這里獲得JSON格式的最新融資貸款。
KivaLoanTableViewController.swift并在開始時聲明兩個變量:
private let kivaLoanURL = "https://api.kivaws.org/v1/loans/newest.json"
private var loans = [Loan] ()
我們剛剛定義了Kiva API的URL,并聲明用于存儲一系列貸款對象的貸款變量。接下來,在同一個文件中插入以下方法:
func getLatestLoans() {
? ? guard let loanUrl = URL(string: kivaLoanURL) else {
? ? ? ? return
? ? }
? ? let request = URLRequest(url: loanUrl)
? ? let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
? ? ? ? if let error = error {
? ? ? ? ? ? print(error)
? ? ? ? ? ? return
? ? ? ? }
? ? ? ? // Parse JSON data
? ? ? ? if let data = data {
? ? ? ? ? ? self.loans = self.parseJsonData(data: data)
? ? ? ? ? ? // Reload table view
? ? ? ? ? ? OperationQueue.main.addOperation({
? ? ? ? ? ? ? ? self.tableView.reloadData()
? ? ? ? ? ? })
?}
? })
?task.resume()
}
func parseJsonData(data: Data) -> [Loan] {
? ? var loans = [Loan]()
? ? do {
? ? ? ? let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary
? ? ? ? // Parse JSON data
? ? ? ? let jsonLoans = jsonResult?["loans"] as! [AnyObject]
? ? ? ? for jsonLoan in jsonLoans {
? ? ? ? ? ? let loan = Loan()
? ? ? ? ? ? loan.name = jsonLoan["name"] as! String
? ? ? ? ? ? loan.amount = jsonLoan["loan_amount"] as! Int
? ? ? ? ? ? loan.use = jsonLoan["use"] as! String
? ? ? ? ? ? let location = jsonLoan["location"] as! [String:AnyObject]
? ? ? ? ? ? loan.country = location["country"] as! String
? ? ? ? ? ? loans.append(loan)
? ? ? ? }
? ? } catch {
? ? ? ? print(error)
}
return loans
}
這兩種方法構成了應用程序的核心部分。這兩種方法協(xié)作調(diào)用Kiva API,檢索JSON格式的最新貸款,并將JSON格式的數(shù)據(jù)轉換為貸款對象數(shù)組。讓我們詳細地討論一下。在getLatestLoans方法中,我們首先用Kiva貸款API的URL實例化URL結構。初始化返回一個可選的。這就是為什么我們使用guard關鍵字來查看可選項是否具有值。如果不是,我們只是返回并跳過方法中的所有代碼。
接下來,我們用load URL創(chuàng)建一個URLSession。URLSession類為通過HTTP和HTTPS處理在線內(nèi)容提供了api。共享會話對于發(fā)出簡單的HTTP/HTTPS請求來說已經(jīng)足夠了。如果您必須支持自己的網(wǎng)絡協(xié)議,那么URLSession還為您提供了創(chuàng)建自定義會話的選項。
URLSession的一個優(yōu)點是,您可以添加一系列會話任務來處理數(shù)據(jù)加載,以及從服務器上傳和下載文件和數(shù)據(jù)獲取(例如JSON數(shù)據(jù)獲取)。
通過會話,您可以安排三種類型的任務:數(shù)據(jù)任務(URLSessionDataTask)用于檢索數(shù)據(jù)到內(nèi)存,下載任務(URLSessionDownloadTask)用于下載文件到磁盤,以及上傳任務(URLSessionUploadTask)用于從磁盤上傳文件。在這里,我們使用data任務從Kiva.org檢索內(nèi)容。要向會話添加數(shù)據(jù)任務,我們使用特定的URL請求調(diào)用dataTask方法。添加任務后,會話將不會采取任何操作。您必須調(diào)用resume方法(即task.resume())來啟動數(shù)據(jù)任務。
與大多數(shù)網(wǎng)絡API一樣,URLSession API也是異步的。一旦請求完成,它通過調(diào)用完成處理程序返回數(shù)據(jù)(以及錯誤)。
在完成處理程序中,在返回數(shù)據(jù)之后,我們檢查是否有錯誤。如果沒有找到錯誤,我們調(diào)用parseJsonData方法。
返回的數(shù)據(jù)是JSON格式。我們創(chuàng)建了一個名為parseJsonData的助手方法,用于將給定的json格式的數(shù)據(jù)轉換為一個貸款對象數(shù)組。Foundation框架提供了JSONSerialization類,它能夠?qū)SON轉換為Foundation對象,并將Foundation對象轉換為JSON。在代碼片段中,我們使用給定的JSON數(shù)據(jù)調(diào)用jsonObject方法來執(zhí)行轉換。
當將JSON格式的數(shù)據(jù)轉換為對象時,通常將頂級項轉換為字典或數(shù)組。在本例中,Kiva API返回數(shù)據(jù)的頂層被轉換為字典。您可以使用關鍵貸款訪問貸款數(shù)組。
你怎么知道用什么Key?
您可以使用一個JSON瀏覽器來引用API文檔或測試JSON數(shù)據(jù)(例如http://jsonviewer.stack.hu)。如果您已經(jīng)將Kiva API加載到JSON瀏覽器中,這里是結果的一個例外
{
? "paging": {
? ? "page": 1,
? ? "total": 5297,
? ? "page_size": 20,
? ? "pages": 265
? },
? "loans": [
? ? {
? ? ? "id": 794429,
? ? ? "name": "Joel",
? ? ? "description": {
? ? ? ? "languages": [
? ? ? ? ? "es",
? ? ? ? ? "en"
? ? ? ? ]
? ? ? },
? ? ? "status": "fundraising",
? ? ? "funded_amount": 0,
? ? ? "basket_amount": 0,
? ? ? "image": {
? ? ? ? "id": 1729143,
? ? ? ? "template_id": 1
? ? ? },
? ? ? "activity": "Home Appliances",
? ? ? "sector": "Personal Use",
? ? ? "use": "To buy home appliances.",
? ? ? "location": {
? ? ? ? "country_code": "PE",
? ? ? ? "country": "Peru",
? ? ? ? "town": "Ica",
? ? ? ? "geo": {
? ? ? ? ? "level": "country",
? ? ? ? ? "pairs": "-10 -76",
? ? ? ? ? "type": "point"
? ? ? ? }
? ? ? },
? ? ? "partner_id": 139,
? ? ? "posted_date": "2015-11-20T08:50:02Z",
? ? ? "planned_expiration_date": "2016-01-04T08:50:02Z",
? ? ? "loan_amount": 400,
? ? ? "borrower_count": 1,
? ? ? "lender_count": 0,
? ? ? "bonus_credit_eligibility": true,
? ? ? "tags": [
? ? ? ]
? ? },
? ? {
? ? ? "id": 797222,
? ? ? "name": "Lucy",
? ? ? "description": {
? ? ? ? "languages": [
? ? ? ? ? "en"
? ? ? ? ]
? ? ? },
? ? ? "status": "fundraising",
? ? ? "funded_amount": 0,
? ? ? "basket_amount": 0,
?"image": {
? ? ? ? "id": 1732818,
? ? ? ? "template_id": 1
? ? ? },
? ? ? "activity": "Farm Supplies",
? ? ? "sector": "Agriculture",
? ? ? "use": "To purchase a biogas system for clean cooking",
? ? ? "location": {
? ? ? ? "country_code": "KE",
? ? ? ? "country": "Kenya",
? ? ? ? "town": "Gatitu",
? ? ? ? "geo": {
? ? ? ? ? "level": "country",
? ? ? ? ? "pairs": "1 38",
? ? ? ? ? "type": "point"
? ? ? ? }
? ? ? },
? ? ? "partner_id": 436,
? ? ? "posted_date": "2016-11-20T08:50:02Z",
? ? ? "planned_expiration_date": "2016-01-04T08:50:02Z",
? ? ? "loan_amount": 800,
? ? ? "borrower_count": 1,
? ? ? "lender_count": 0,
? ? ? "bonus_credit_eligibility": false,
? ? ? "tags": [
? ? ? ]
? ? },
? ? ...
正如您從上面的代碼中看到的,分頁和貸款是兩個頂級項目。一旦JSONSerialization類轉換JSON數(shù)據(jù),結果(即jsonResult)將作為字典返回,頂級項作為鍵。這就是為什么我們可以使用關鍵貸款來獲得貸款。這一行代碼供您參考:
let jsonLoans = jsonResult?["loans"] as! [AnyObject]
返回貸款數(shù)組(即jsonLoans)后,我們對數(shù)組進行循環(huán)。每個數(shù)組項(即jsonLoan)都被轉換為字典。在循環(huán)中,我們從每個字典中提取貸款數(shù)據(jù),并將它們保存到一個loan對象中。同樣,通過研究JSON結果,您可以找到鍵(以黃色突出顯示)。特定結果的值存儲為AnyObject。之所以使用AnyObject,是因為JSON值可以是字符串、Double、Boolean、數(shù)組、字典或null。這就是為什么要將值向下轉換為特定類型的原因,例如String和Int。最后,我們將貸款對象放入貸款數(shù)組中,這是方法的返回值。
for jsonLoan in jsonLoans {
? ? var loan = Loan()
? ? loan.name = jsonLoan["name"] as! String
? ? loan.amount = jsonLoan["loan_amount"] as! Int
? ? loan.use = jsonLoan["use"] as! String
? ? let location = jsonLoan["location"] as! [String: AnyObject]
? ? loan.country = location["country"] as! String
? ? loans.append(loan)
}
在解析JSON數(shù)據(jù)并返回貸款數(shù)組之后,我們調(diào)用reloadData方法來重新加載表。您可能想知道為什么我們需要調(diào)用OperationQueue.main。在主線程中添加和執(zhí)行數(shù)據(jù)重載。數(shù)據(jù)任務的完成處理程序中的代碼塊在后臺線程中執(zhí)行。如果在后臺線程中調(diào)用reloadData方法,則不會立即重新加載數(shù)據(jù)。為了確保響應的GUI更新,應該在主線程中執(zhí)行此操作。這就是為什么我們調(diào)用OperationQueue.main。addOperation方法和請求在主隊列中運行reloadData方法。
OperationQueue.main.addOperation({
? ? self.tableView.reloadData()
})
注意:您還可以使用dispatch_async函數(shù)在主線程中執(zhí)行一個代碼塊。但是根據(jù)Apple的說法,建議在dispatch_async上使用OperationQueue。一般來說,蘋果建議使用最高級的api,而不是降級為低級的api。
在TableView中顯示貸款
有了貸款數(shù)組,我們最不需要做的就是在表視圖中顯示數(shù)據(jù)。”在kivaloantableviewcontroller中更新以下方法:
override func numberOfSections(in tableView: UITableView) -> Int {
? ? // Return the number of sections
? ? return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
? ? // Return the number of rows
? ? return loans.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
? ? let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! KivaLoanTableViewCell
? ? // Configure the cell...
? ? cell.nameLabel.text = loans[indexPath.row].name
? ? cell.countryLabel.text = loans[indexPath.row].country
? ? cell.useLabel.text = loans[indexPath.row].use
? ? cell.amountLabel.text = "$\(loans[indexPath.row].amount)"
? ? return cell
}
如果您熟悉UITableView的實現(xiàn),那么上面的代碼非常簡單。在tableView(_:cellForRowAt:)方法中,我們從貸款數(shù)組中檢索貸款信息,并在自定義表單元格中填充它們。需要注意的一件事是下面的代碼:
"$\(loans[indexPath.row].amount)"
在某些情況下,您可能希望創(chuàng)建一個字符串(例如$)和integer(例如loan [indexPath.row].amount])。Swift提供了一種強大的方法來創(chuàng)建這些類型的字符串,稱為字符串插值。您可以使用上面的語法。
最后,在viewDidLoad方法中插入以下代碼行來開始獲取貸款數(shù)據(jù):?getLatestLoans()
運行App

在 這里下載Xcode 項目
Codable 介紹
Swift 4引入了一種使用可編碼方式對JSON數(shù)據(jù)進行編碼和解碼的新方法。我們將使用這種新方法重寫演示應用程序的JSON解碼部分。在我們開始修改之前,讓我給你一個關于可編程的基本步驟。如果您查看可編程的文檔,它只是協(xié)議組合的一種類型別名:
typealias Codable = Decodable & Encodable?
Decodable 和 Encodable 是您需要處理的兩個實際協(xié)議。但是,為了方便起見,我們通常使用這個類型別名來處理JSON編碼和解碼。
首先,與傳統(tǒng)的編碼/解碼JSON方法相比,使用 codable 的優(yōu)勢是什么?如果你回到前一節(jié)再讀一遍代碼,你會發(fā)現(xiàn)我們必須手動解析JSON數(shù)據(jù),將其轉換為字典并創(chuàng)建貸款對象。
Codable為開發(fā)者提供了一種不同的解碼(或編碼)JSON的方式,從而簡化了整個過程。只要您的類型符合可編碼協(xié)議,以及新的JSONDecoder,您就能夠?qū)SON數(shù)據(jù)解碼到指定的實例中。圖4.3演示了如何使用JSONDecoder將示例貸款數(shù)據(jù)解碼為一個貸款實例。

JSON?解碼
為了讓您更好地了解可編程的工作方式,讓我們啟動一個playground項目并編寫一些代碼。創(chuàng)建了playground項目后,聲明以下json變量:?
let json = """
{
"name": "John Davis",
"country": "Peru",
"use": "to buy a new collection of clothes to stock her shop before the holidays.",
"amount": 150
}
"""
我們首先從基礎開始。這里我們定義一個非常簡單的JSON數(shù)據(jù),包含4個條目。前三個項目的值的類型字符串最后一個Int類型。邊注,如果這是你第一次看到一雙三重引號("""), Swift4中介紹了這個語法來聲明與多行字符串。
接下來,像這樣寫貸款結構:
struct Loan: Codable {
? ? var name: String
? ? var country: String
? ? var use: String
? ? var amount: Int
}
這種貸款結構與我們在上一節(jié)中定義的貸款結構非常相似,只不過它采用了codable 協(xié)議。您還應該注意,屬性名與JSON數(shù)據(jù)的名稱匹配.