精通 SwiftUI - iOS 17 版

第 21 章
使用 JSON、滑桿與資料篩選

JSON 是 JavaScript Object Notation 的縮寫,是用戶端- 伺服器應用程式中用於資料交換的通用資料格式。即使我們是行動裝置 App 的開發者,也不可避免地要使用 JSON,因為幾乎所有的 Web API 或後端網頁服務都使用 JSON 作為資料交換的格式。

在本章中,我們將討論當使用 SwiftUI 框架建立 App 時如何使用 JSON。如果你對於不了解 JSON 的話,我建議看一下在《iOS 程式設計進階攻略》一書中的 免費試閱章節 ,這裡會詳細解釋在 Swift 中處理 JSON 的兩種不同方法。

圖 21.1. 範例App
圖 21.1. 範例App

和往常一樣,為了掌握 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 所示。

圖 21.2. 滑桿控制元件
圖 21.2. 滑桿控制元件

了解 JSON 與 Codable

首先,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 實例。

圖 21.3. JSONDecoder 解碼 JSON 資料,並將其轉換為一個 Loan 實例
圖 21.3. JSONDecoder 解碼 JSON 資料,並將其轉換為一個 Loan 實例

使用 JSONDecoder 與 Codable

在建立範例 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 實例,其中填滿了解碼後的值。

圖 21.4. 在主控台中顯示解碼後的借貸資料
圖 21.4. 在主控台中顯示解碼後的借貸資料

讓我們再次研究程式碼片段。我們實例化一個 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 回應。首先,更新 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 方法,以取得與指定的編碼鍵相關的資料,即namelocationuseamount

要解碼一個特定值,我們使用特定鍵(例如:.name )和關聯型別(例如:String.self ) 來呼叫 decode 方法。nameuseamount 的解碼非常簡單。對於 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 物件,並將其儲存至 LoanStoreloans 陣列中。如果你印出 loans,應該會看到如圖 21.5 所示的類似訊息。

圖 21.5. 印出loans 陣列
圖 21.5. 印出loans 陣列

以上就是使用 Swift 解碼 JSON 的方式。

注意:作為參考,Playgrounds 項目包含在最終下載檔內。 你可以在本章末端找到下載連結。

建立 Kiva 貸款 App

好的,你現在應該已經了解如何處理 JSON 解碼,讓我們開始建立一個範例 App,來看如何運用剛剛所學的技術。我們將從建立模型類別來開始,該模型類別儲存從 Kiva 取得的所有最新貸款資料。對於使用者介面的實作,我們將稍後處理。

取得 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 物件,並將其儲存至 LoanStoreloans 陣列中,這就是為什麼我們建立上述 LoanStore 的原因。程式碼看起來與我們之前建立的 LoanStore 結構非常相似,但是它採用 Decodable 協定而不是Codable 協定。

如果你研究 Codable 的文件,它只是一個協定組成的型別別名:

typealias Codable = Decodable & Encodable

DecodableEncodable 是你需要實際使用的兩個協定。由於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上不好發揮作用。

圖 21.6. Xcode 指出 LoanStore 並沒有遵循 Decodable 協定的錯誤訊息
圖 21.6. Xcode 指出 LoanStore 並沒有遵循 Decodable 協定的錯誤訊息

要修正這個錯誤,需要做一些額外的工作。當使用 @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 列舉來明確指定要解碼的鍵,然後我們實作自訂的初始器來處理解碼。

好的,錯誤現在已經修正了,那下一步呢?

呼叫 Web API

到目前為止,我們只設定了 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。

圖 21.7. 範例 App 的使用者介面
圖 21.7. 範例 App 的使用者介面

而且,我們將UI 分成三個視圖,而不是在同一個檔案中撰寫UI 的程式碼:

  • ContentView.swift - 這是呈現貸款清單的主視圖。
  • LoanCellView.swift - 這是單元格視圖。
  • LoanFilterView.swift - 這是顯示篩選選項的視圖。

我們從單元格視圖開始。在專案導覽器中,在 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 類似。

圖 21.8. 貸款單元格視圖
圖 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.9. 在清單視圖中呈現貸款資料
圖 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 所示。

圖 21.10. 用於設定顯示條件的篩選器視圖
圖 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的貸款紀錄。

圖 21.11. 顯示篩選器視圖
圖 21.11. 顯示篩選器視圖

本章小結

本章介紹了很多的內容,你應該知道如何使用 Web API、解析 JSON 內容,以及在清單視圖中顯示資料,我們還簡要介紹了滑桿控制元件的用法。

如果你之前使用 UIKit 開發過 App,你可能會對 SwiftUI 的簡單性感到驚訝。再看一下 ContentView 的程式碼,只需要 40 行的程式碼就可以建立清單視圖。最重要的是,你不需要手動處理 UI 更新及傳送資料,一切都在背後運作。

為了參考,你可以至下列的網址下載完整的專案: