第 4 章
JSON 與 Swift Codable 的介紹
首先,什麼是 JSON ? JSON(JavaScript Object Notation 的縮寫)是一個以文字為主、輕量型(lightweight),並且容易使用來儲存以及交換資料的方式。通常用來呈現結構性的資料,以及在客戶端及 Server 端做資料交換時,做為替代 XML 格式的另一種服務。許多我們每天使用的網頁服務都是以 JSON 為主的 API。大部份的 iOS App 包含 Twitter、Facebook 以及 Flicker,都是以 JSON 格式從後端(backend)網站服務來傳送資料。
舉例來說 以 JSON 來表示電影物件的方式像這樣:
{
"title": "The Amazing Spider-man",
"release_date": "03/07/2012",
"director": "Marc Webb",
"cast": [
{
"name": "Andrew Garfield",
"character": "Peter Parker"
},
{
"name": "Emma Stone",
"character": "Gwen Stacy"
},
{
"name": "Rhys Ifans",
"character": "Dr. Curt Connors"
}
]
}
如以上所示,JSON 格式是更加易於閱讀的,並且比 XML 還來得容易解析。我這裡不準備說明 JSON 的細節,這並非本章的學習目的,倘若你想要學習更多有關 JSON 的技術,可以查閱 JSON Guides(http://www.json.org/)。
自從 iOS 5 釋出後,iOS SDK 讓開發者能夠更容易地取得以及解析 JSON 資料。它內建一個類別,稱為 NSJSONSerialization
,可以讓你輕易地將 JSON 格式的資料轉換為物件(Object)。稍後在本章,我將會告訴你,我們將示範如何使用 API 來解析一些 JSON 格式的資料,並以網路服務(web service)來回傳資料。當你了解運作原理之後,可以很容易地整合其他的免費/付費網路服務來建構 App。
在 Swift 4 (或者 iOS 11),Apple 推出了 Codable
協定來簡化整個 JSON 檔案(Archives)與序列化(Serialization)的程序。我們將會進一步談到這個新功能,並了解如何在 JSON 解析中應用它。
範例 App
如同先前,我們建立一個範例 App 稱為 KivaLoan。為何這個 App 稱為 KivaLoan 的理由,是因為我們應用了 Kiva.org 的 JSON API。倘若你沒有聽過 Kiva,這是一個非營利組織,主要任務是串連人們提供借貸來減輕貧困。它讓每個人至少可以借貸出 25 美元,在世界各地幫忙建立機會。Kiva 提供了免費的網路 API 讓開發者可以存取它們的資料。針對我們的範例 App,我們將會呼叫以下的 Kiva API 來取得最近的融資貸款,並以表格視圖來做呈現:
https://api.kivaws.org/v1/loans/newest.json
Quick note:自從 iOS 9 開始,Apple 導入了一個新的功能,稱作 App傳輸安全標準(App Transport Security ,簡稱 ATS),目的是改善 App 與網路服務間連結的安全性。預設中所有的對外連結應該要遵循 HTTPS,否則你的 App 將不允許連結網路服務。另外,你也可以在 Info.plist 中加入一個名為 NSAllowsArbitraryLoads 的鍵(Key),並設定其值為 YES 來關閉 ATS,如此便能透過 HTTP 來連結網路 API。 不過,你必須注意是否在你的 App 中使用 NSAllowsArbitraryLoads 。在 iOS 10,Apple 進一步針對強制所有 iOS App 要使用 ATS。在2017年一月,所有的 iOS App 都要遵循ATS。換句話說,倘若你的 App 連結了其他的網路服務,這個連結必定是要透過 HTTPS 才行。倘若你的 App 無法遵循這個要求,Apple 將不允許在App Store上架。
以上API 的回傳資料是 JSON格式,這裏為其中一例:
loans: (
{
activity = Retail;
"basket_amount" = 0;
"bonus_credit_eligibility" = 0;
"borrower_count" = 1;
description = {
languages = (
fr,
en
);
};
"funded_amount" = 0;
id = 734117;
image = {
id = 1641389;
"template_id" = 1;
};
"lender_count" = 0;
"loan_amount" = 750;
location = {
country = Senegal;
"country_code" = SN;
geo = {
level = country;
pairs = "14 -14";
type = point;
};
};
name = "Mar\U00e8me";
"partner_id" = 108;
"planned_expiration_date" = "2016-08-05T09:20:02Z";
"posted_date" = "2016-07-06T09:20:02Z";
sector = Retail;
status = fundraising;
use = "to buy fabric to resell";
},
....
....
)
你將學會如何使用 NSJSONSerialization
類別來轉換 JSON 資料為物件。這簡單到不可思議,等一下你便會明白我的意思。
為了讓你能夠專注在 JSON 實作的學習,你可以至 http://www.appcoda.com/resources/swift59/KivaLoanStarter.zip,下載專案模版來進行。我已經建立了 App 的雛形給你。這是一個簡單的表格 App,用來顯示由 Kiva.org 所提供的貸款清單。這個專案模板包含了預建的 Storyboard,與替表格視圖控制器(Table View Controller)以及 Prototype Cell 所自訂的類別。倘若你執行這個模板,它會回傳一個空的表格 App。
建立 JSON 資料模型
我們會先建立一個貸款模型的類別。它不需要載入 JSON,而最好的做法是建立一個單獨的類別(或者是結構)來儲存資料模型,Loan
類別代表了在 KivaLoan App中的貸款資訊,並用來儲存由 Kiva.org 所回傳的貸款資訊。為了簡單起見,我們將不使用貸款的所有回傳資料。我們將只是顯示以下的貸款欄位
- 貸款申請人姓名
name = "Mar\U00e8me";
- 貸款申請人國家
location = {
country = Senegal;
"country_code" = SN;
geo = {
level = country;
pairs = "14 -14";
type = point;
};
};
- 貸款的用途
use = "to buy fabric to resell";
- 貸款總數
"loan_amount" = 750;
這些欄位已經足夠填入表格視圖中的標籤。現在使用 Swift 檔案模板來建立一個新類別,將其命名為Loan.swift
,並且宣告 Loan
結構如下:
struct Loan: Hashable {
var name: String = ""
var country: String = ""
var use: String = ""
var amount: Int = 0
}
JSON 支援幾個基本資料型態,包含數字、字串、布林值、陣列與物件(一個附有鍵值配對的關聯陣列)。
針對我們所使用的貸款欄位,貸款總數是以數值(numeric value)來儲存在 JSON 格式資料中。這是為什麼我們宣告 amount
屬性為 Int
型態,而其他剩餘欄位則宣告為 String
型態。
使用 Kiva API 取得貸款資料
依前面所述,Kiva API 可以免費使用且不需要註冊。你需要連到以下網址來取得最近貸款籌湊狀況的 JSON 格式。
https://api.kivaws.org/v1/loans/newest.json
好的,我們來看要如何呼叫 Kiva API 並解析回傳資料。首先開啟 KivaLoanTableViewController.swift
並且在最開始的地方宣告兩個變數:
private let kivaLoanURL = "https://api.kivaws.org/v1/loans/newest.json"
private var loans = [Loan]()
我們只是定義 Kiva API 的 URL 並且宣告 loans
變數來儲存 Loan 物件的陣列。接著插入以下的方法至相同的檔案中:
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
}
// 解析 JSON 資料
if let data = data {
self.loans = self.parseJsonData(data: data)
// 重新載入表格資料
OperationQueue.main.addOperation({
self.updateSnapshot()
})
}
})
task.resume()
}
func parseJsonData(data: Data) -> [Loan] {
var loans = [Loan]()
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary
// 解析 JSON 資料
let jsonLoans = jsonResult?["loans"] as! [AnyObject]
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)
}
} catch {
print(error)
}
return loans
}
這兩個方法構成 App 的核心部分。這些方法共同協作來呼叫 Kiva API,取得最新貸款資料的 JSON 格式,並將這個 JSON 格式轉譯為 Loan
物件的陣列。我們逐步來了解其細項內容。
在 getLatestLoans
方法中,我們先以 Kiva Loan API 的 URL來實例化 URL
結構,此初始化回傳一個 Optional。這也是為何我們使用guard
關鍵字來檢查此 Optional 是否有值。如果沒有值的話,我們則返回,並略過方法中的所有程式。
接下來,我們以載入的 URL 建立一個 URLSession
。這個 URLSession
類別提供 API,透過 HTTP與 HTTPS來處理線上內容。這個共享的 session 已經足夠處理簡單的 HTTP/HTTPS 請求,倘若你必須支援你自己的網路協定, URLSession
也會提供你建立自訂session的選項。
其中 URLSession
最棒的一件事是你可以加入一連串的 session 任務來處理資料載入,上傳與下載檔案以及從伺服器端取得資料(例如 JSON資料擷取)。
有了 session,你可以安排三種型態的任務(task):將資料取回放置記憶體的 data tasks(URLSessionDataTask
)、下載檔案至磁碟的 download tasks(URLSessionDownloadTask
),以及從磁碟上傳檔案的 upload tasks(URLSessionUploadTask
)。這裏我們使用 data task 從 kiva.org 取回內容。要將 data task 加入 session 的話,我們以指定 URL 請求(request)呼叫 dataTask
方法。在加上這個任務之後,seesion便不再進行任何動作。你要呼叫 resume 方法(也就是task.resume()
)來初始化 data task。
跟大部分的網路API 一樣,URLSession
API 是非同步(asynchronous)。 在請求完成後,它會呼叫你的完成處理器閉包(completion handler closure)來回傳資料(以及錯誤)。
在完成處理器內,資料回傳之後,緊接著我們立即檢查是否有任何的錯誤並且呼叫 parseJsonData
方法。
這個方法以 JSON 格式回傳。我們建立了一個輔助方法(helper method)稱為 parseJsonData
,用來轉換給定的 JSON 格式資料做為 Loan
物件的陣列。Foundation 框架(framework)提供了JSONSerialization
類別,可以將 JSON 轉換為 Foundation 物件,以及轉換 Foundation 物件為 JSON 。 在程式中, 我們給予 JSON 資料來呼叫 jsonObject
方法以執行轉換。
在轉換 JSON 格式資料為物件後,最上層的項目通常轉換為一個字典(Dictionary)或陣列。在這個例子中,Kiva API 所回傳資料的最上層被轉換成一個字典。你可以使用 loans
做為鍵(key) 來存取 loans的陣列。
你怎麼會知道要使用哪個鍵呢?
你可以參考 API 文件或者在測試 JSON 資料時使用 JSON 瀏覽器(例如 http://jsonviewer.stack.hu)。倘若你有載入 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": [
]
},
...
你可以參考以上的程式,paging
以及 loans
是兩個最上層的項目。一旦 JSONSerialization
類別轉換為 JSON 資料,其結果(也就是 jsonResult
)會以最上層項目做為鍵的字典來回傳。這也是為什麼我們可以使用 loans
這個鍵來存取loans 的陣列。如以下這行程式做為參考:
let jsonLoans = jsonResult?["loans"] as! [AnyObject]
有了 loans 的陣列(也就是 jsonLoans)回傳,我們透過迴圈查詢這個陣列,每一個陣列項目(也就是 jsonLoan)轉換成字典。在這迴圈中,我們將每一筆貸款資料從字典中取出,並儲存在 Loan
物件。同樣的,你可以透過研究 JSON 結果找到這些 key。這個特定結果的值以 AnyObject
來儲存。使用 AnyObject
是因為 JSON 值可能是一個 String、Double、Bool、Array、Dictionary 或者空值。這也是為何必須將值轉型(downcast)為特定型態像是 String
或Int
。最後,我們將 loan
物件放進 loans
陣列中,也就是這個方法的回傳值。
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 資料解析完與 loans 的陣列回傳之後,我們呼叫 reloadData
方法來重新載入表格。你可能想知道為何我們需要呼叫 OperationQueue.main.addOperation
並在主執行緒(main thread)執行資料重載。
在 data task 的完成處理器裡的這段程式是在背景執行緒(background thread)執行。倘若你只在背景執行緒中呼叫 updateSnapshot
方法,資料的重載則不會馬上發生。為了確保 GUI 能反應更新,此操作需要在主執行緒完成。這也是為何我們呼叫 OperationQueue.main.addOperation
方法並請求在主佇列(main queue)執行 updateSnapshot
方法的原因。
OperationQueue.main.addOperation({
self.updateSnapshot()
})
Quick note: 你也可以在主執行緒使用dispatch_async
函數來執行程式區塊 ,但是 Apple 推薦使用OperationQueue
勝於使用dispatch_async
。 一般的通例是,Apple 會推薦使用高階的 API,而不是往下使用低階的。
我們還沒有實現 updateSnapshot()
方法,這將在下一節中討論。
在表格視圖顯示貸款資料
loans
陣列準備就位後,最後一件事就是在表格視圖中顯示資料。首先,在 KivaLoanTableViewController
中宣告一個 Section
列舉(Enum):
enum Section {
case all
}
接下來,在同一類別中插入以下方法:
func configureDataSource() -> UITableViewDiffableDataSource<Section, Loan> {
let cellIdentifier = "Cell"
let dataSource = UITableViewDiffableDataSource<Section, Loan>(
tableView: tableView,
cellProvider: { tableView, indexPath, loan in
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! KivaLoanTableViewCell
cell.nameLabel.text = loan.name
cell.countryLabel.text = loan.country
cell.useLabel.text = loan.use
cell.amountLabel.text = "$\(loan.amount)"
return cell
}
)
return dataSource
}
func updateSnapshot(animatingChange: Bool = false) {
// Create a snapshot and populate the data
var snapshot = NSDiffableDataSourceSnapshot<Section, Loan>()
snapshot.appendSections([.all])
snapshot.appendItems(loans, toSection: .all)
dataSource.apply(snapshot, animatingDifferences: animatingChange)
}
如果你了解如何使用 UITableViewDiffableDataSource
實現表格視圖,則上面的程式碼非常簡單,這是現在推薦的顯示表格視圖數據的方法。 如果你是 UITableViewDiffableDataSource
的新手,請參考我們的初學者書籍。
簡而言之,configureDataSource
方法用於從 loans
陣列中檢索貸款信息並將它們顯示到自定義表格單元格中。
有件事要注意的是以下這行程式:
"$\(loan.amount)"
在某些情況,你會以字串( 例 如 $)加上整數( 例 如 "$\(loan.amount)"
) 來建立一個字串。Swift 提供了一個強大的方式來建立這樣的字串,稱為作字串插值(string interpolation)。你可以使用以上的語法來達成。
接下來,宣告一個 dataSource
變數,如下所示:
lazy var dataSource = configureDataSource()
最後,在 viewDidLoad
方法插入以下的程式,來插入以下的貸款資料:
getLatestLoans()
編譯與執行App
現在是時候測試 App 了。在模擬器中編譯與執行。App 開啟後,將會從 Kiva.org 取得最新的貸款資訊,並呈現在表格視圖中。
為了方便參考,你可以從以下的網址下載完整的 Xcode 專案: http://www.appcoda.com/resources/swift59/KivaLoan.zip 。
Codable 介紹
自 Swift 4 開始,Apple 推出了一個新的方式,使用 Codable
來針對 JSON 資料進行編碼(encode)與解碼(decode)。我們將使用這個新方法來重寫範例 App 中 JSON 解碼的部分
在修改之前,我們來介紹一下有關Codable
的基礎內容。如果你進一步查閱一下 Codable
文件,它只是一個協定組成的型態別名(type alias):
typealias Codable = Decodable & Encodable
Decodable
與 Encodable
是你需要處理的兩的協定。不過,為了方便起見,我們通常參考這個型態別名來處理 JSON 編碼與解碼。
首先,使用 Codable
來處理 JSON 的編碼 / 解碼,跟傳統的作法相比來看有什麼優點呢?如果你回到前一節,並重新閱讀一下程式碼,你將會注意到我們必須手動解析 JSON 資料,將它轉換為字典並建立 Loan
物件。
Codable
簡化了整個程序,提供了開發者不同的 JSON 解碼(或編碼)方式。只要你的型態遵循 Codable
協定,配合 JSONDecoder
,你便能夠將 JSON 資料解碼至你指定的實例。圖 4.3 介紹了使用JSONDecoder
將貸款資料解碼至 Loan
實例的範例。
本文摘自《iOS 17 App程式設計進階攻略》一書。如果你想繼續閱讀和下載完整程式碼,你可以從AppCoda網站購買完整電子版,全書範例檔皆可下載。