JSON 是 JavaScript Object Notation 的縮寫,是用戶端- 伺服器應用程式中用於資料交換的通用資料格式。即使我們是行動裝置 App 的開發者,也不可避免地要使用 JSON,因為幾乎所有的 Web API 或後端網頁服務都使用 JSON 作為資料交換的格式。
在本章中,我們將討論當使用 SwiftUI 框架建立 App 時如何使用 JSON。如果你對於不了解 JSON 的話,我建議看一下在《iOS 程式設計進階攻略》一書中的 免費試閱章節 ,這裡會詳細解釋在 Swift 中處理 JSON 的兩種不同方法。
和往常一樣,為了掌握 JSON 及其相關的 API 知識,你將建立一個簡單的 JSON App, 該 App 利用 Kiva.org.提供的 JSON API。若是你沒有聽過 Kiva, 這是一個非營利組織,其使命是透過借貸將人們聯擊在一起,以減輕貧困問題;Kiva 讓每個人借出至少 25 美元,來幫助世界各地的人創造機會。Kiva 為開發者提供了免費的 Web API 來存取他們的資料。對於我們的範例 App,我們將呼叫一個免費的 Kiva API 來取得最近的募資借款,並在清單視圖中顯示,如圖 21.1 所示。
除此之外,我們將示範滑桿(Slider )的用法,滑桿是SwiftUI 提供的眾多內建 UI 控制元件之一。你將在 App 中實作一個資料篩選選項,以讓使用者可以篩選清單中的貸款資料,如圖 21.2 所示。
首先,JSON 格式是什麼樣子呢?如果你不了解 JSON,則開啟瀏覽器,並指向下列由 Kiva 提供的 Web API:
https://api.kivaws.org/v1/loans/newest.json
你應該會看到下列的內容:
{
"loans": [
{
"activity": "Fruits & Vegetables",
"basket_amount": 25,
"bonus_credit_eligibility": false,
"borrower_count": 1,
"description": {
"languages": [
"en"
]
},
"funded_amount": 0,
"id": 1929744,
"image": {
"id": 3384817,
"template_id": 1
},
"lender_count": 0,
"loan_amount": 250,
"location": {
"country": "Papua New Guinea",
"country_code": "PG",
"geo": {
"level": "town",
"pairs": "-9.4438 147.180267",
"type": "point"
},
"town": "Port Moresby"
},
"name": "Mofa",
"partner_id": 582,
"planned_expiration_date": "2020-04-02T08:30:11Z",
"posted_date": "2020-03-03T09:30:11Z",
"sector": "Food",
"status": "fundraising",
"tags": [],
"themes": [
"Vulnerable Groups",
"Rural Exclusion",
"Underfunded Areas"
],
"use": "to purchase additional vegetables to increase her currrent sales."
},
...
"paging": {
"page": 1,
"page_size": 20,
"pages": 284,
"total": 5667
}
}
你的顯示結果可能不是相同的格式,但這就是 JSON 回應的樣式。如果你使用的是 Chrome,則可以下載並安裝一個名為「JSON Formatter」(http://link.appcoda.com/json-formatter )的外掛程式,來美化 JSON 回應。
或者,你可以在 Mac 上使用下列的指令,來對格式化 JSON 資料:
curl https://api.kivaws.org/v1/loans/newest.json | python -m json.tool > kiva-loans-data.txt
這將格式化 JSON 回應,並將其儲存到文字檔中。
現在你對 JSON 有一些了解了,我們如何在 Swift 中解析 JSON 資料呢?從 Swift 4 開始, Apple 導入一個編碼及解碼 JSON 資料的新方式,即採用了一個名為 Codable
的協定。
Codable
為開發者提供一個不同的方式來解碼(或編碼)JSON,從而簡化了整個過程。只要你的型別遵循Codable
協定以及新的 JSONDecoder
,你就能夠將 JSON 資料解碼到你指定的實例(instance )中。
圖 21.3 說明了使用 JSONDecoder
,將範例借貸資料解碼為一個 Loan
實例。
在建立範例 App 之前,我們在 Playgrounds 上嘗試 JSON 解碼。啟動 Xcode,至選單並選擇「File → New → Projects」來建立一個新專案。如往常一樣,使用「App」模板,並將專案名稱命名為「SwiftUIKivaLoan」或是你所喜愛的其他名稱。之後,右鍵點選專案導覽器中的 SwiftUIKivaLoan 資料夾,然後選擇 New file...。 在 iOS 標籤下,選擇 Blank Playground 建立一個新的 Playground。當你建立你的 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 解析的新手,我們簡單說明一下。上面是一個簡化的 JSON 回應,類似於上一小節所示的回應。
要解析資料,宣告 Loan
結構如下:
struct Loan: Codable {
var name: String
var country: String
var use: String
var amount: Int
}
如你所見,該結構採用了 Codable
協定。而且,結構中定義的變數與 JSON 回應的鍵值相符,這是讓解碼器知道如何解碼資料的方法。
現在,讓我們來看看它的魔力 !
繼續在Playground 檔案中插入下列的程式碼:
let decoder = JSONDecoder()
if let jsonData = json.data(using: .utf8) {
do {
let loan = try decoder.decode(Loan.self, from: jsonData)
print(loan)
} catch {
print(error)
}
}
如果你執行這個專案,則應該在主控台中看到一個訊息,這是 Loan
實例,其中填滿了解碼後的值。
讓我們再次研究程式碼片段。我們實例化一個 JSONDecoder
實例,然後將 JSON 字串轉換為 Loan
。而魔法發生在下列這行程式碼中:
let loan = try decoder.decode(Loan.self, from: jsonData)
你只需要呼叫解碼器的 decode
方法來對 JSON 資料解碼,並指定要解碼的值的型別(即 Loan.self
)。解碼器會自動解析 JSON 資料,並將其轉換為一個 Loan
物件。
很酷,是吧?
現在,讓我們進入更複雜的內容。如果屬性名稱與 JSON 的鍵不同的話,該怎麼辦?你如何定義映射(mapping )呢?
舉例而言,我們修改 json
變數如下:
let json = """
{
"name": "John Davis",
"country": "Peru",
"use": "to buy a new collection of clothes to stock her shop before the holidays.",
"loan_amount": 150
}
"""
如你所見,amount
這個鍵現在改為 loan_amount
。為了解碼 JSON 資料,你可以將屬性名稱從 amount
改為loan_amount
。不過,我們真的想要保留名稱 amount
,在這種的情況下,我們如何定義映射呢?
要定義鍵與屬性名稱之間的映射,你需要宣告一個名為「CodingKeys」的列舉, CodingKeys
列舉具有一個 String
型別的原始值,並遵循 CodingKey
協定。
現在,更新 Loan
結構如下:
struct Loan: Codable {
var name: String
var country: String
var use: String
var amount: Int
enum CodingKeys: String, CodingKey {
case name
case country
case use
case amount = "loan_amount"
}
}
在列舉中,你定義了模型的所有屬性名稱,以及其在 JSON 資料中的對應鍵。例如:amount
定義為映射loan_amount
這個鍵,如果屬性名稱與 JSON 資料的鍵相同,則可以省略這個指定。
現在,你應該了解基礎知識了,讓我們深入研究並解碼一個更切合實際的 JSON 回應。首先,更新 json
變數如下:
let json = """
{
"name": "John Davis",
"location": {
"country": "Peru",
},
"use": "to buy a new collection of clothes to stock her shop before the holidays.",
"loan_amount": 150
}
"""
我們已加入了 location
這個鍵,它具有巢狀 JSON 物件以及 country
巢狀鍵。那麼,我們如何從巢狀物件中解碼 country
的值呢?
我們修改 Loan
結構如下:
struct Loan: Codable {
var name: String
var country: String
var use: String
var amount: Int
enum CodingKeys: String, CodingKey {
case name
case country = "location"
case use
case amount = "loan_amount"
}
enum LocationKeys: String, CodingKey {
case country
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
let location = try values.nestedContainer(keyedBy: LocationKeys.self, forKey: .country)
country = try location.decode(String.self, forKey: .country)
use = try values.decode(String.self, forKey: .use)
amount = try values.decode(Int.self, forKey: .amount)
}
}
和我們之前所做的類似,我們必須定義一個列舉 CodingKeys
。對於 country
這個 case, 我們指定要映射location
鍵。而要處理巢狀的 JSON 物件,我們需要定義另一個列舉,在上面的程式碼中,我們將其命名為LocationKeys
,並宣告與巢狀物件的 country
鍵相符的 country
這個 case。
因為它不是直接映射,我們需要實作 Decodable
協定的初始器,來處理所有屬性的解碼。在 init
方法中,我們首先使用 CodingKeys.self
呼叫解碼器的 container
方法,以取得與指定的編碼鍵相關的資料,即name
、location
、use
與 amount
。
要解碼一個特定值,我們使用特定鍵(例如:.name )和關聯型別(例如:String.self ) 來呼叫 decode
方法。name
、use
與 amount
的解碼非常簡單。對於 country
屬性,解碼有點棘手,我們必須使用LocationKeys.self
呼叫 nestedContainer
方法,來取得巢狀的 JSON 物件。從回傳的值,我們進一步解碼country
的值。
以上為使用巢狀物件來解碼JSON 資料的方式。
從 Kiva API 所回傳的 JSON 資料中,通常不只一筆貸款,多筆貸款以陣列的形式建構, 現在我們來看如何使用Codable 解碼 JSON 物件的陣列。
首先,修改 json
變數如下:
let json = """
{
"loans":
[{
"name": "John Davis",
"location": {
"country": "Paraguay",
},
"use": "to buy a new collection of clothes to stock her shop before the holidays.",
"loan_amount": 150
},
{
"name": "Las Margaritas Group",
"location": {
"country": "Colombia",
},
"use": "to purchase coal in large quantities for resale.",
"loan_amount": 200
}]
}
"""
在上面的範例中,json
變數中有兩筆貸款資料,你如何將其解碼為 Loan
的陣列呢?
為此,宣告另一個名稱為 LoanStore
的結構,該結構也採用 Codable
協定:
struct LoanStore: Codable {
var loans: [Loan]
}
該 LoanStore
只有一個 loans
屬性,它與 JSON 資料的 loans
鍵相符。並且,其型別定義為 Loan
的陣列。
要解碼貸款資料,將下列這行程式碼:
let loan = try decoder.decode(Loan.self, from: jsonData)
修改為:
let loanStore = try decoder.decode(LoanStore.self, from: jsonData)
解碼器將自動解碼 loans
JSON 物件,並將其儲存至 LoanStore
的 loans
陣列中。如果你印出 loans
,應該會看到如圖 21.5 所示的類似訊息。
以上就是使用 Swift 解碼 JSON 的方式。
注意:作為參考,Playgrounds 項目包含在最終下載檔內。 你可以在本章末端找到下載連結。
好的,你現在應該已經了解如何處理 JSON 解碼,讓我們開始建立一個範例 App,來看如何運用剛剛所學的技術。我們將從建立模型類別來開始,該模型類別儲存從 Kiva 取得的所有最新貸款資料。對於使用者介面的實作,我們將稍後處理。
首先,使用「Swift File」模板來建立一個新檔案,並將其命名為 Loan.swift
。這個檔案儲存了採用 Codable 協定來進行 JSON 解碼的 Loan
結構。
在檔案中插入下列的程式碼:
struct Loan: Identifiable {
var id = UUID()
var name: String
var country: String
var use: String
var amount: Int
init(name: String, country: String, use: String, amount: Int) {
self.name = name
self.country = country
self.use = use
self.amount = amount
}
}
extension Loan: Codable {
enum CodingKeys: String, CodingKey {
case name
case country = "location"
case use
case amount = "loan_amount"
}
enum LocationKeys: String, CodingKey {
case country
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
let location = try values.nestedContainer(keyedBy: LocationKeys.self, forKey: .country)
country = try location.decode(String.self, forKey: .country)
use = try values.decode(String.self, forKey: .use)
amount = try values.decode(Int.self, forKey: .amount)
}
}
程式碼與我們在前一節中所討論的幾乎相同。我們只是使用擴展(extension )來採用 Codable
協定,除了Codable
協定之外,這個結構也採用了 Identifiable
協定,並具有預設為 UUID()
的id 屬性。稍後,我們將會使用 SwiftUI 的 List
控制元件來呈現這些貸款資料, 這就是為什麼我們讓這個結構採用 Identifiable
協定的原因。
接下來, 使用「Swift File」模板來建立另一個檔案, 並將其命名為「LoanStore. swift」。這個類別是負責連接到 Kiva 的 Web API、解碼 JSON 資料,並將資料儲存在本地端。
我們來逐步撰寫 LoanStore
類別, 如此你就可以更加了解我是如何實作的。在 LoanStore.swift
中插入下列的程式碼:
class LoanStore: Decodable {
var loans: [Loan] = []
}
稍後,解碼器將解碼 loans
JSON 物件,並將其儲存至 LoanStore
的 loans
陣列中,這就是為什麼我們建立上述 LoanStore
的原因。程式碼看起來與我們之前建立的 LoanStore
結構非常相似,但是它採用 Decodable
協定而不是Codable
協定。
如果你研究 Codable
的文件,它只是一個協定組成的型別別名:
typealias Codable = Decodable & Encodable
Decodable
與 Encodable
是你需要實際使用的兩個協定。由於LoanStore
只有負責處理 JSON 解碼,因此我們採用 Decodable
協定。
如前所述,我們將使用一個清單視圖來顯示 loans
。因此,除了 Decodable
之外,我們必須採用ObservableObject
協定,並使用 @Published
屬性包裹器標記 loans
變數,如下所示:
class LoanStore: Decodable, ObservableObject {
@Published var loans: [Loan] = []
}
如此,當 loans
變數有任何變動時,SwiftUI 將自動管理 UI 的更新。如果你已經忘記什麼是 ObservableObject
,則請再次閱讀第 14 章。
不過,當你加上 @Published
屬性包裹器後,Xcode 就會顯示一個錯誤訊息,如圖 21.6 所示。Decodable
(或Codable )協定在 @Published
上不好發揮作用。
要修正這個錯誤,需要做一些額外的工作。當使用 @Published
屬性包裹器時,我們需要手動實作 Decodable
所需的方法。如果你研究文件(https://developer.apple.com/documentation/swift/decodable)),可以採用以下的方法:
init(from decoder: Decoder) throws
實際上,在解碼巢狀的 JSON 物件時,我們已經實作這個方法,現在更新類別如下:
class LoanStore: Decodable, ObservableObject {
@Published var loans: [Loan] = []
enum CodingKeys: CodingKey {
case loans
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
loans = try values.decode([Loan].self, forKey: .loans)
}
init() {
}
}
我們加上 CodingKeys
列舉來明確指定要解碼的鍵,然後我們實作自訂的初始器來處理解碼。
好的,錯誤現在已經修正了,那下一步呢?
到目前為止,我們只設定了 JSON 解碼的所有內容,但我們尚未使用 Web API。現在於 LoanStore
類別中宣告一個新變數,來儲存 Kiva API 的URL:
private static var kivaLoanURL = "https://api.kivaws.org/v1/loans/newest.json"
Next, insert the following methods in the class:
func fetchLatestLoans() {
guard let loanUrl = URL(string: Self.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 {
DispatchQueue.main.async {
self.loans = self.parseJsonData(data: data)
}
}
})
task.resume()
}
func parseJsonData(data: Data) -> [Loan] {
let decoder = JSONDecoder()
do {
let loanStore = try decoder.decode(LoanStore.self, from: data)
self.loans = loanStore.loans
} catch {
print(error)
}
return loans
}
fetchLatestLoans()
方法透過使用 URLSession
來連接到 Web API。當它接收到 API 回傳的資料後,它會傳送資料給 parseJsonData
方法來解碼 JSON,並轉換貸款資料為 Loan
陣列。
你可能想知道為什麼我們需要使用 DispatchQueue.main.async
來包裹這行程式碼?
DispatchQueue.main.async {
self.loans = self.parseJsonData(data: data)
}
當呼叫 Web API 時, 該操作是在背景佇列中執行。在此,loans
變數標記為 @Published
,這意味著對於變數的任何修改,SwiftUI 將觸發使用者介面的更新。而且, UI 更新需要在主佇列中執行,這就是我們使用DispatchQueue.main.async
包裹它的原因。
現在,我們已經建立了用於取得貸款資料的類別,讓我們繼續進行使用者介面的實作。我猜想你可能忘了UI 的外觀,請參考圖 21.7,這是我們將要建立的 UI。
而且,我們將UI 分成三個視圖,而不是在同一個檔案中撰寫UI 的程式碼:
我們從單元格視圖開始。在專案導覽器中,在 SwiftUIKivaLoan
按右鍵,選擇「New file...」,然後選取「SwiftUI View」模板,並將檔案命名為 LoanCellView.swift
。
更新 LoanCellView
如下:
struct LoanCellView: View {
var loan: Loan
var body: some View {
HStack(alignment: .top) {
VStack(alignment: .leading) {
Text(loan.name)
.font(.system(.headline, design: .rounded))
.bold()
Text(loan.country)
.font(.system(.subheadline, design: .rounded))
Text(loan.use)
.font(.system(.body, design: .rounded))
}
Spacer()
VStack {
Text("$\(loan.amount)")
.font(.system(.title, design: .rounded))
.bold()
}
}
.frame(minWidth: 0, maxWidth: .infinity)
}
}
這個視圖帶入 Loan
,並渲染單元格視圖。該程式碼一目了然,但如果你想要預覽單元格視圖,你將需要修改LoanCellView_Previews
如下:
#Preview {
LoanCellView(loan: Loan(name: "Ivan", country: "Uganda", use: "to buy a plot of land", amount: 575))
}
我們實例化一個虛構的貸款資料,並傳送至單元格視圖做渲染。只要你將預覽設定為 Selectable 模式,你的預覽面板看起來應該與圖 21.8 類似。
現在回到 ContentView.swift
來實作清單視圖,首先,宣告一個名為 loanStore
的變數:
@ObservedObject var loanStore = LoanStore()
由於我們要觀察貸款商店的變化並更新UI,因此將 loanStore
標記為 @ObservedObject
屬性包裹器。
接著,更新 body
變數如下:
var body: some View {
NavigationStack {
List(loanStore.loans) { loan in
LoanCellView(loan: loan)
.padding(.vertical, 5)
}
.navigationTitle("Kiva Loan")
}
.task {
self.loanStore.fetchLatestLoans()
}
}
如果你已經閱讀了第 10 章與第 11 章,則應該了解如何顯示清單視圖,並將其嵌入到導覽視圖中,這就是上面的程式碼所做的事情。在視圖出現時將呼叫 task
函數,並且我們呼叫 fetchLatestLoans()
方法來從 Kiva 取得最新的貸款資料。
如果這是你第一次使用 .task ,它與 .onAppear 非常相似。 兩者都允許你在視圖出現時運行異步任務。 主要區別在於.task
會在視圖被銷毀時自動取消任務。 在這種情況下更合適。
現在,在預覽中或模擬器上執行這個 App,你應該能夠看到如圖 21.9 所示的貸款紀錄。
在完成本章之前,我想要介紹如何實作一個篩選功能。這個篩選功能可讓使用者定義最大貸款金額,並只顯示低於該金額的那些紀錄。圖 21.7 為篩選器視圖的範例,使用者可以使用滑桿來設定最大數量。
同樣的,為了讓程式更有組織性,因此為篩選器視圖建立一個新檔案,並將其命名為 LoanFilterView.swift
。
現在更新 LoanFilterView
結構如下:
struct LoanFilterView: View {
@Binding var amount: Double
var minAmount = 0.0
var maxAmount = 10000.0
var body: some View {
VStack(alignment: .leading) {
Text("Show loan amount below $\(Int(amount))")
.font(.system(.headline, design: .rounded))
HStack {
Slider(value: $amount, in: minAmount...maxAmount, step: 100)
.accentColor(.purple)
}
HStack {
Text("\(Int(minAmount))")
.font(.system(.footnote, design: .rounded))
Spacer()
Text("\(Int(maxAmount))")
.font(.system(.footnote, design: .rounded))
}
}
.padding(.horizontal)
.padding(.bottom, 10)
}
}
我假設你完全了解堆疊視圖,因此我將不討論如何使用它們來實現佈局,不過讓我們再多談一下滑桿控制元件,其是 SwiftUI 所提供的一個標準元件,你可以透過傳送滑桿的綁定(Binding )、範圍與滑桿刻度(step )來實例化滑桿。綁定保存滑桿的目前值。以下是用於建立滑桿的範例程式碼:
Slider(value: $amount, in: minAmount...maxAmount, step: 100)
刻度可控制使用者拖曳滑桿時的更改量。如果你讓使用者擁有很好的控制元件,請將刻度設定更小一點。對於上列的程式碼,我們將其設定為「100」。
為了預覽篩選器視圖,更新 #Preview
如下:
#Preview {
LoanFilterView(amount: .constant(10000))
}
現在,你的預覽應該如圖 21.10 所示。
好的,我們已經實作了篩選器視圖,但是我們尚未實作篩選紀錄的實際邏輯。讓我們增強 LoanStore.swift
的篩選功能。
首先,宣告以下的變數,該變數用來儲存貸款紀錄的複本,以作為篩選操作之用。
private var cachedLoans: [Loan] = []
要儲存該副本,請插入下列這行程式碼,並將其放在 self.loans = self.parseJsonData(data: data)
的後面:
self.cachedLoans = self.loans
最後,為篩選建立一個新函數:
func filterLoans(maxAmount: Int) {
self.loans = self.cachedLoans.filter { $0.amount < maxAmount }
}
這個函數帶入最大金額的值,並篩選低於該限額的那些貸款項目。
酷 !我們快完成了。
讓我們回到 ContentView.swift
來顯示篩選器視圖,我們要做的是在右上角加上一個導覽列按鈕。當使用者點擊按鈕時,App 會顯示篩選器視圖。
我們首先宣告兩個狀態變數:
@State private var filterEnabled = false
@State private var maximumLoanAmount = 10000.0
filterEnabled
變數儲存篩選器視圖的目前狀態。預設是設定為「false」,表示沒有顯示篩選器視 圖。maximumLoanAmount
儲存用於顯示的最大貸款金額,任何大於該限額的貸款紀錄都將被隱藏。
接下來,將 NavigationView
的程式碼更新如下:
NavigationStack {
VStack {
if filterEnabled {
LoanFilterView(amount: self.$maximumLoanAmount)
.transition(.opacity)
}
List(loanStore.loans) { loan in
LoanCellView(loan: loan)
.padding(.vertical, 5)
}
}
.navigationTitle("Kiva Loan")
}
我們添加了LoanFilterView
並將其嵌入到 VStack
中。 LoanFilterView
的外觀由 filterEnabled
控制。 當 filterEnabled
設置為 true
時,App將在列表視圖的頂部插入貸款過濾器視圖。 剩下的部分是導覽列按鈕,插入下列的程式碼,並將其放在 .navigationBarTitle("Kiva Loan")
的後面:
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
withAnimation(.linear) {
self.filterEnabled.toggle()
self.loanStore.filterLoans(maxAmount: Int(self.maximumLoanAmount))
}
} label: {
Text("Filter")
.font(.subheadline)
.foregroundColor(.primary)
}
}
}
這將在右上角加入一個導覽列按鈕。當點擊按鈕後,我們切換 filterEnabled
的值來顯示/ 隱藏篩選器視圖。除此之外,我們呼叫 filterLoans
函數來篩選貸款項目。
現在執行 App 來測試它,你應該會在導覽列上看到一個篩選器按鈕,點擊它一次,將帶出篩選器視圖,然後你可以設定新的上限(例如:$500)。再次點擊按鈕,App 只會顯示低於 $500的貸款紀錄。
本章介紹了很多的內容,你應該知道如何使用 Web API、解析 JSON 內容,以及在清單視圖中顯示資料,我們還簡要介紹了滑桿控制元件的用法。
如果你之前使用 UIKit 開發過 App,你可能會對 SwiftUI 的簡單性感到驚訝。再看一下 ContentView 的程式碼,只需要 40 行的程式碼就可以建立清單視圖。最重要的是,你不需要手動處理 UI 更新及傳送資料,一切都在背後運作。
為了參考,你可以至下列的網址下載完整的專案: