開發者一般都會問兩個有關 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