開發者一般都會問兩個有關 SwiftUI 的常見問題。 首先是如何在SwiftUI項目使用 Core Data, 而另一個常見問題是如何在 SwiftUI 項目中使用 UIKit 視圖。 在本章中,我們將透過在 Todo App 建立搜索欄以學習如何將UIKit內的 UISearchBar 整合至 SwiftUI 項目。
如果你是 UIKit 的新手,UISearchBar 是UIKit框架的一個內置組件,它允許開發者為數據搜索呈現一個搜索欄。 圖 23.1 顯示了 iOS 中的標準搜索欄。 然而,在SwiftUI 剛推出時,它並沒有附帶這個標準的 UI 組件。 要在 SwiftUI 項目(例如我們的 ToDo App)加入搜索欄,其中一種方法就是使用 UIKit 中的UISearchBar
組件。
那麼,我們如何在 SwiftUI 中整合 UIKit 視圖或控制器?
為了向後兼容,Apple 在 iOS SDK 中引入了幾個新協議,即 UIViewRepresentable 和 UIViewControllerRepresentable。 使用這些協議,你可以包裝 UIKit 視圖(或視圖控制器)並使其可用於你的 SwiftUI 項目。
為了了解它是如何工作的,我們將為Todo App加入搜索功能 。 我們將在App標題正下方添加一個搜索欄,讓用戶輸入搜索詞來過濾待辦事項。
首先,請在 https://www.appcoda.com/resources/swiftui5/SwiftUIToDoList.zip 下載 ToDo 項目。 我們將在 ToDoList 項目之上再作修改加入搜索功能。 如果你還沒有閱讀第 22 章,我建議你先閱讀。 這將幫助你更理解我們將在下面討論的主題,特別是如果你沒有 SwiftData 的實作經驗。
要在 SwiftUI 中使用 UIKit 視圖,你需要使用 UIViewRepresentable 協議包裝視圖。 基本上,你只需要在 SwiftUI 中建立一個 struct
,它採用協議來建立和管理一個 UIView
對象。 這是 UIKit 視圖自訂包裝器(custom wrapper)的框架:
struct CustomView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
// Return the UIView object
}
func updateUIView(_ uiView: some UIView, context: Context) {
// Update the view
}
}
在實際應用中,你將some UIView
替換為你想要包裝的 UIKit 視圖。 比方說,我們想在 UIKit 中使用UISearchBar
。 程式碼可以這樣寫:
struct SearchBar: UIViewRepresentable {
func makeUIView(context: Context) -> UISearchBar {
return UISearchBar()
}
func updateUIView(_ uiView: UISearchBar, context: Context) {
// Update the view
}
}
在makeUIView
方法中,我們回一個UISearchBar
的實體(instance)。 這就是如何將 UIKit 視圖整合至 SwiftUI。 要使用 SearchBar
,你可以像對待任何 SwiftUI 視圖一樣。以下是一個個例子:
struct ContentView: View {
var body: some View {
SearchBar()
}
}
現在回到 ToDoList 項目,我們會為App加入搜索欄。 首先,我們將為搜索欄創建一個新檔案。 在項目導航器中,右鍵單擊 View 文件夾並選擇 New File.... 選擇 SwiftUI View 模板並將文件命名為SearchBar.swift
。
將內容替換為以下程式碼:
import SwiftUI
struct SearchBar: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UISearchBar {
let searchBar = UISearchBar()
searchBar.searchBarStyle = .minimal
searchBar.autocapitalizationType = .none
searchBar.placeholder = "Search..."
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: Context) {
uiView.text = text
}
}
struct SearchBar_Previews: PreviewProvider {
static var previews: some View {
SearchBar(text: .constant(""))
}
}
該程式碼與上一節中顯示的程式碼類似,但有以下區別:
UISearchBar
時,我們並沒有使用預設外觀,而是使用最簡單(.minimal
)的樣式,並停用自動大寫和更新其佔位符的文字。makeUIView
方法負責建立和初始化視圖,而 updateUIView
方法則負責更新 UIKit 視圖的狀態。 每當 SwiftUI 中的狀態發生變化時,框架都會自動呼叫 updateUIView
方法來更新視圖的配置。 在這種情況下,每當你在 SwiftUI 中更新搜索文字時,都會呼叫該方法並更新UISearchBar
的text
。現在切換到ContentView.swift
。 宣告一個狀態變數來存放搜索文字:
@State private var searchText = ""
要顯示搜索欄,請在 List
之前加入以下程式碼:
SearchBar(text: $searchText)
.padding(.top, -20)
SearchBar
就像任何其他 SwiftUI 視圖一樣,你可以應用.padding
等修飾器來調整佈局。 如果你在模擬器中運行該App,你應該會看到一個搜索欄,但它尚未起能正常運作。
如你所見,在 SwiftUI App中顯示 UIKit 視圖並不是一件很複雜的事。 雖然這樣說,要樣搜索欄運作就是另一回事。 目前,你可以在搜索字段中輸入,但App尚未能執行查詢。 這個App應該在使用者輸入搜索文字時即時搜索待辦事項。
那麼,我們如何知道用戶正在輸入搜索文字呢?
搜索欄有一個名為UISearchBarDelegate
的配套協議。 該協議提供了多種管理搜索文字的方法。 特別是,每當用戶更改搜索文字時,都會呼叫以下方法:
optional func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
為了使搜索欄運作,我們必須採用UISearchBarDelegate
協議。如你沒有任何UIKit開發經驗, 這就是複雜的地方。
到目前為止,我們只討論了 UIViewRepresentable
協議中的幾個方法。 如果你需要在 UIKit 中使用委託並與 SwiftUI 溝通,則必須實現 makeCoordinator
方法並提供一個 Coordinator
實體(instance)。 這個Coordinator
充當了 UIView 的委託和 SwiftUI 之間的橋樑。 讓我們看一下程式碼,這樣你就會明白它的含義。
在 Search Bar
結構(Search Bar.swift
),建立一個 Coordinator
類別並實現 makeCoordinator
方法,如下所示:
func makeCoordinator() -> Coordinator {
Coordinator($text)
}
class Coordinator: NSObject, UISearchBarDelegate {
@Binding var text: String
init(_ text: Binding<String>) {
self._text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
searchBar.showsCancelButton = true
text = searchText
print("textDidChange: \(text)")
}
}
makeCoordinator
方法只回一個 Coordinator
的實體。 Coordinator
採用UISearchBarDelegate
協議並實現searchBar(_:textDidChange:)
方法。 如前所述,每次用戶更改搜尋文字時都會呼叫此方法。 因此,當有更新時,我們透過更新 text
綁定將其傳回 SwiftUI。 我特意在方法裡加了一個print
語句,方便以後我們測試app的時候可以看到變化。
現在我們有一個採用了 UISearchBarDelegate
協議的 Coordinator,我們需要再做一個修改。 在 makeUIView
方法中,加入以下一行程式碼,將協調器指定給搜索欄:
searchBar.delegate = context.coordinator
就是這樣! 再次執行App並在輸入搜尋文字。 你應該會在控制台中看到textDidChange:
的訊息。
你是否點擊了取消按鈕? 如果你嘗試過,就會知道它又是不能正常運作。 要解決這個問題,我們必須在Coordinator
中實現以下方法:
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
text = ""
searchBar.resignFirstResponder()
searchBar.showsCancelButton = false
searchBar.endEditing(true)
}
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
searchBar.showsCancelButton = true
return true
}
單擊取消按鈕時觸發第一種方法。 在程式碼中,我們呼叫 resignFirstResponder()
來關閉鍵盤並告訴搜索欄結束編輯。 第二種方法確保在使用者點擊搜尋文字時出現 Cancel 按鈕。
你可以在模擬器中運行App來試試這個修改。 在編輯時,點擊 Cancel 按鈕應該會自動關閉軟體鍵盤。
我們現在可以檢索搜尋文字並處理了取消按鈕。 可惜,搜索欄仍然無法正常使用。 這就是我們將在本節中講解的內容。 對於這個App,有幾種方法可以執行搜索:
filter
函數對 todoItems
執行搜索FetchRequest
執行搜尋基本上第一種方法對於這個App來說已經足夠,因為 todoItems
與儲存在數據庫中的待辦事項同步。 但我還是想向你示範如何使用FetchRequest
來執行搜尋。 因此,我們將一齊研究這兩種方法。
filter
函數在 Swift 中,你可以使用 filter
函數以迴圈來重複查詢一個集合,然後回傳一個內含符合搜尋條件項目的新陣列。 以下是一個例子:
todoItems.filter({ $0.name.contains("Buy") })
filter
函數內的其中一個參數是給呼叫者以閉包形式指定過濾條件。 舉例,以上的程式碼就會搜出所有包含「Buy」字名稱的項目。
To implement the search, we can replace the ForEach
loop of the List
like this:為了實作搜尋功能,我們可以像這樣更改 List
的 ForEach
迴圈:
ForEach(todoItems.filter({ searchText.isEmpty ? true : $0.name.contains(searchText) })) { todoItem in
ToDoListRow(todoItem: todoItem)
}
.onDelete(perform: deleteTask)
在 filter
函數的閉包中,我們首先檢查搜尋文字是否有值。 如果沒有,我們簡單地回 true
。這意味所有待辦事項都會被加入新陣列中。 相反,就只回包含搜尋字串的待辦事項。
酷 !你可以準備啟動你的 App 來測試搜尋功能。 輸入搜尋文字,App就會搜尋相關記錄。
在本章中,你學習如何使用 UIViewRepresentable
協議將 UIKit 視圖整合至 SwiftUI 項目。 雖然 SwiftUI 仍然很新並且沒有附帶所有標準 UI 組件,但這種向後兼容性允許你利用舊框架使用任何視圖。
我們還講解了兩種實作數據搜尋的方法, 你現在應該知道如何使用 filter
函數並了解如何建立動態讀取請求。
在本章所準備的範例檔中,有最後完整的 Xcode 專案,可供你參考:
https://www.appcoda.com/resources/swiftui5/SwiftUIToDoListUISearchBar.zip