精通 SwiftUI - iOS 17 版

第 24 章
建立搜尋欄視圖並使用自訂綁定(Custom Binding)

之前,我們向你講解了如何利用 UIKit 框架的UISearchBar 來實現搜索欄。 你有沒有想過從頭開始自己建立一個? 如果仔細看看搜尋欄,要自己做出來並不太難。 所以,讓我們在本章中嘗試構建一個 SwiftUI 版本的搜尋欄。

你不僅可以學習如何建立搜尋欄視圖,我們還將會和你講解如何使用自訂綁定。 之前我們討論過綁定,但還沒有向你展示如何建立自定義綁定。 當你需要為綁定(Binding)加入額外的程式邏輯時,自訂綁定就特別有用。 另外,你也會學習如何在 SwiftUI 中關閉軟體鍵盤。

圖 24.1 就是我們將要建立的搜索欄,外觀與 UIKit 中的 UISearchBar 非常相似。 同樣地,這個搜索欄也會有個 Cancel 按鈕,當使用者開始輸入搜尋文字時就會出現。

圖 24.1. 利用 SwiftUI 自己建立搜尋欄
圖 24.1. 利用 SwiftUI 自己建立搜尋欄

實作搜尋欄 UI

我們將會修改之前的項目轉為我們自己建立的搜尋欄。 因此,請首先從 https://www.appcoda.com/resources/swiftui5/SwiftUIToDoListUISearchBar.zip 下載啟動項目。 編譯一次以確保它有效。 該App應該向你顯示一個搜索欄,但是,該欄來自 UIKit。 我們將把它轉換成一個完全使用 SwiftUI 構建的搜尋欄視圖。

打開SearchBar.swift,這是我們關注的文件。 我們將重寫整段程式碼,但保持其名稱不變。 我們仍然稱它為「SearchBar」,它仍然接受搜尋文字的綁定作為參數。 對於呼叫者(即 ContentView),沒有什麼需要變更, 用法還是這樣:

SearchBar(text: $searchText)

現在,讓我們從 UI 開始做起。 如果你想挑戰自己,請停止閱讀並嘗試自己實作搜尋欄 UI。 這個使用者介界非常簡單。 它由一個文字欄、幾個圖標和取消按鈕組成。

如果你不知道 UI 是如何建立的,就讓我們一起寫出來吧。 請將 SearchBar.swift 中的 SearchBar 結構改成這樣:

struct SearchBar: View {
    @Binding var text: String

    @State private var isEditing = false

    var body: some View {
        HStack {

            TextField("Search ...", text: $text)
                .padding(7)
                .padding(.horizontal, 25)
                .background(Color(.systemGray6))
                .cornerRadius(8)
                .overlay(
                    HStack {
                        Image(systemName: "magnifyingglass")
                            .foregroundStyle(.gray)
                            .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                            .padding(.leading, 8)

                        if isEditing {
                            Button(action: {
                                self.text = ""
                            }) {
                                Image(systemName: "multiply.circle.fill")
                                    .foregroundStyle(.gray)
                                    .padding(.trailing, 8)
                            }
                        }
                    }
                )
                .padding(.horizontal, 10)
                .onTapGesture {
                    withAnimation {
                        self.isEditing = true
                    }
                }


            if isEditing {
                Button(action: {
                    self.isEditing = false
                    self.text = ""

                }) {
                    Text("Cancel")
                }
                .padding(.trailing, 10)
                .transition(.move(edge: .trailing)) 
            }
        }
    }
}

首先,我們宣告了兩個變數:一個是搜尋文字的綁定,另一個則用於存放搜尋狀態(正在編輯與否)的變數。

我們使用 HStack 來佈局文字欄和 Cancel 按鈕。 對於文字欄,我們覆蓋了一個放大鏡圖案和交叉圖案(即multiply.circle.fill),它僅在搜尋欄處於編輯模式時顯示。 Cancel 按鈕也是如此,當使用者點擊搜尋欄時才會出現。

為了預覽搜尋欄,還請加入以下程式碼:

#Preview {
    SearchBar(text: .constant(""))
}

當加入程式碼後,你應該能夠預覽搜尋欄。 現在按播放 鈕進行測試。 當你選擇搜尋欄時,取消 按鈕就會出現。

圖 24.2. 預覽搜尋欄
圖 24.2. 預覽搜尋欄

更重要的是,搜尋欄已經可以使用了! 在模擬器上運行App並輸入搜尋字,它就會根據顯示相關的搜索結果。

圖 24.3. 搜尋欄已正式運作
圖 24.3. 搜尋欄已正式運作

關閉鍵盤

如你所見,使用 SwiftUI 建立我們自己的搜尋欄並不太難。 在搜尋欄運作時,我們必須解決一個小問題。 你有否嘗試點擊取消按鈕? 它確實清除了搜尋文字。 然而,軟鍵盤並沒有消失。

為了解決這個問題,我們需要在 Cancel 按鈕的 action 中添加一行程碼:

// 關閉鍵盤
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)

在程式碼中,我們呼叫了 sendAction 方法來關閉鍵盤。 你現在可以使用模擬器運行App。 當你點擊取消按鈕時,它就會清除搜尋文字並關閉軟體鍵盤。

使用自訂綁定(Custom Binding)

SwiftUI 版本的搜尋欄已經可以正常使用了,不過我想藉此機會與你討論自訂綁定。 在 SearchBar.swift ,我們像這樣宣告搜尋文字的綁定:

@Binding var text: String

它非常適合我們當前程式的運作。 但是讓我來問問你, 如果我們在讀取或寫入此綁定時需要添加額外的邏輯,那可以怎麼辦? 例如,如何將使用者輸入的文字自動變成大寫?

Swift 有一個內置的功能可以將字串變為大寫。 你可以使用字串的 capitalized 屬性取得大寫文字串。但 問題是我們如何更新text的綁定?

在這種情況下,你需要在SearchBar.swift中建立一個自定義綁定,如下所示:

private var searchText: Binding<String> {

    return Binding<String>(
        get: {
            self.text.capitalized

        }, set: {
            self.text = $0
        }
    )
}

在上面的程式碼中,我們建立了一個名為searchText的自定義綁定,其中包含讀取和寫入綁定值的閉包。 對於get 部分,我們通過capitalized 屬性來自定義text 的綁定值。 這就是我們如何將使用者的搜尋字轉成大寫。 至於set 部分,我們不做任何更改,只將其設置為原始值。 但是,如果在設置綁定時需要添加額外的邏輯,就需要修改set中的程碼。

題外話,你可以省略 return 關鍵字,像這樣寫就可以:

private var searchText: Binding<String> {

    Binding<String>(
        get: {
            self.text.capitalized

        }, set: {
            self.text = $0
        }
    )
}

以防你沒有留意,這是自 Swift 5.1 就有的一項新功能,

我們仍在將 text 綁定傳遞給 TextField。 在此更改生效之前,我們需要再進行一個小修改。 更改TextField中的參數並確保將searchText作為綁定傳遞:

TextField("Search ...", text: searchText)

現在在模擬器上運行該App。 在搜尋欄輸入幾個詞,App就會自動把每個單詞的第一個字母變成大寫。

圖 24.4. 自動把每個單詞的第一個字母變成大寫
圖 24.4. 自動把每個單詞的第一個字母變成大寫

總結

在本章中,我們向你展示了另一種實現搜尋欄的方法。 如你所見,單是用 SwiftUI 建立一個搜尋欄並不困難。 你還學習了如何建立自定義綁定。 當你在設置或檢索綁定值時需要添加額外的程序時,這就非常有用。

在本章所準備的範例檔中,有最後完整的 Xcode 專案,可供你參考:

https://www.appcoda.com/resources/swiftui5/SwiftUIToDoListSearchBarView.zip