[譯]UISearchController 教程:開始使用

嘎嘣脆發表於2018-03-03

(原文出處:https://www.raywenderlich.com/157864/uisearchcontroller-tutorial-getting-started)

注:本指南已經由 Tom Elliott 適配 Xcode 9,Swift 4,以及 iOS 11。原版教程編寫者是 Andy Pereira

劃過複雜混亂的列表既慢又使人心煩。當資料來源巨大的時候,提供搜尋功能搜尋指定條目是對使用者十分重要的功能。UIKit 提供了 UISearchBar,允許你無縫整合到 UINavigationItem ,並可快速響應資訊過濾。

在本教程中,你將基於標準 TableView 構建一個可搜尋的 Candy app。你將賦予 tableView 搜尋和動態過濾能力,以及新增範圍欄,這些全部依賴 UISearchController 的特性。在最後,我們將討論如何讓你的 App 更加友好,以及更滿足使用者的需要。

準備好了麼?開始吧!

開始

下載初始專案原始碼並開啟。此時已經設定了一個導航控制器。在 Xcode 專案導航,選擇專案 CandySearch,然後選擇 target CandySearch,然後找signing 欄目中,配置你的開發者資訊。編譯執行 App,你將看到一個空列表:

[譯]UISearchController 教程:開始使用

回到 Xcode,檔案 Canday.swift 中包含一個結構體 Candy,這是你要顯示在列表中的元素。這個結構體有兩個屬性:category 和 name。

當使用者在 App 中搜尋糖果時,使用的是 name 欄位作為搜尋條件。在本教程快結束的時候,你將看到 category 字串作為 Scope Bar 來實現分類。

構建 Table View

開啟 MasterViewController.swiftcandies 屬性將用來儲存所有糖果物件,供使用者搜尋。說到這兒,是時候建立糖果物件啦!在本教程中,你只需要建立有限數量的物件,用來演示 search bar 的工作;在正式 App 中,你可能有數千個物件用於搜尋。但是不管是有數千物件用於搜尋,還是數個物件用於搜尋,使用方法是不變的。伸縮性很好。

新增以下程式碼在viewDidLoad,用於構建你的糖果物件陣列:

candies = [
  Candy(category:"Chocolate", name:"Chocolate Bar"),
  Candy(category:"Chocolate", name:"Chocolate Chip"),
  Candy(category:"Chocolate", name:"Dark Chocolate"),
  Candy(category:"Hard", name:"Lollipop"),
  Candy(category:"Hard", name:"Candy Cane"),
  Candy(category:"Hard", name:"Jaw Breaker"),
  Candy(category:"Other", name:"Caramel"),
  Candy(category:"Other", name:"Sour Chew"),
  Candy(category:"Other", name:"Gummi Bear"),
  Candy(category:"Other", name:"Candy Floss"),
  Candy(category:"Chocolate", name:"Chocolate Coin"),
  Candy(category:"Chocolate", name:"Chocolate Egg"),
  Candy(category:"Other", name:"Jelly Beans"),
  Candy(category:"Other", name:"Liquorice"),
  Candy(category:"Hard", name:"Toffee Apple")
]
複製程式碼

編譯執行你的專案。因為 delegate 和 dataSource 已經被實現,所以此時 tableView 已經存在資料:

[譯]UISearchController 教程:開始使用

在 table 中隨意選擇一行將展示該糖果的詳情:

[譯]UISearchController 教程:開始使用

糖果有很多,查詢起來需要一些時間,所以你需要一個 UISearchBar。

介紹 UISearchController

如果你看過 UISearchController 的文件,你會發現這是個懶惰的物件。關於搜尋的工作其實它啥也沒做。這個類提供了一組使用者所期待的那種標準的互動操作方式。

UISearchController 通過代理協議連線讓 App 知道使用者的輸入。具體的字串匹配和結果過濾過濾必須由你來完成。

雖然這有點嚇人,但編寫自定義搜尋功能讓你嚴格控制在 App 中的返回結果,你的使用者將開心的用上智慧、快速的搜尋。

如果你之前編寫過搜尋功能,你也許會熟悉UISearchDisplayController。從 iOS8 開始,這個類已經被標記為廢棄。UISearchController 被推薦使用且簡化了整個的搜尋流程。

不幸的是,截止到本文編寫,Interface Builder 還並不支援 UISearchController,所以你必須使用程式碼構建你的 UI。

MasterViewController.swift 檔案中,增加一個新的屬性:

let searchController = UISearchController(searchResultsController: nil)
複製程式碼

如果使用 nil 初始化 UISearchController,那麼搜尋結果也使用相同的檢視來進行現實。如果這裡指定了一個非空的 View Controller,那它將被用於顯示搜尋結果。

響應搜尋框使用者輸入的資訊,需要給 MasterViewController 實現 UISearchResultUpdating 協議定義的方法。給 MasterViewController.swift 增加以下擴充套件:

extension MasterViewController: UISearchResultsUpdating {
  // MARK: - UISearchResultsUpdating Delegate
  func updateSearchResults(for searchController: UISearchController) {
    // TODO
  }
}
複製程式碼

updateSearchResults(for:)是唯一一個需要你的類實現的 UISearchResultUpdating 協議方法。我們等下就會把細節填滿。

接下來,需要設定一些引數給 searchController。仍然是在MasterViewController.swift,在viewDidLoad()super.viewDidLoad() 呼叫之後:

  // Setup the Search Controller
  searchController.searchResultsUpdater = self
  searchController.obscuresBackgroundDuringPresentation = false
  searchController.searchBar.placeholder = "Search Candies"
  navigationItem.searchController = searchController
  definesPresentationContext = true
複製程式碼

總結一下這些程式碼都做了什麼:

  1. UISearchControllersearchResultsUpdater 屬性指向一個 UISearchResultsUpdating 協議。響應使用者在 UISearchBar 中的輸入由這個協議完成。

  2. 預設情況下,UISearchController 彈出來的時候,檢視的背景是模糊的。這是因為我們傳遞了別的 ViewController 作為 searchResultsController。在我們剛剛的程式碼中,我們使用了相同的 ViewController 用於搜尋結果返回,所以檢視的背景沒有模糊。

  3. 設定佔位符文字。

  4. 在 iOS 11 中,新增searchBarNavigationItem。目前在Interface Builder還不能直接操作。

  5. 設定了 ViewControllerdefinesPresentationContexttrue,確保當 UISearchController 為活躍狀態時,使用者導航到了新的 ViewController(如從搜尋結果), 搜尋欄還在螢幕最上方。

過濾搜尋結果

設定完之後 SearchController,需要新增一些程式碼使它工作。首先增加下面這個屬性給MasterViewController:

var filteredCandies = [Candy]()
複製程式碼

這個屬性將儲存使用者搜尋用的 candies 資料集合。 接下來,新增下面這些輔助方法給 MasterViewController 類:

// MARK: - Private instance methods
  
func searchBarIsEmpty() -> Bool {
  // Returns true if the text is empty or nil
  return searchController.searchBar.text?.isEmpty ?? true
}
  
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
  filteredCandies = candies.filter({( candy : Candy) -> Bool in
    return candy.name.lowercased().contains(searchText.lowercased())
  })

  tableView.reloadData()
}
複製程式碼

searchBarIsEmpty() 是一個便利方法。filterContentForSearchText(_:scope:) 方法根據searchText文字過濾candies陣列,然後將過濾後的結果生成filterdCandies陣列。不要擔心scope引數,下一節我們來出來它。

filter() 方法接受一個閉包(candy: Candy) -> Bool。在這個閉包中,我們判斷陣列是不是我們想要的,如果是,返回true,否則返回false,我們根據返回結果生成陣列。

我們使用lowercased()方法把文字先轉換成小寫。使用contains(_:)方法對文字內容進行判斷。

註釋:大多數時候,使用者不想去刻意區分大小寫版本的搜尋結果,所以我們對使用者輸入的內容和蛋糕名稱都進行小寫處理然後比較。你輸入`Chocolate`或者`chocolate`都應該能找到蛋糕。這很有用,對吧?
複製程式碼

還記得 UISearchResultsUpdating協議麼?你留了一個TODOupdateSearchResults(for:)方法裡面。現在補充一個方法呼叫,更新搜尋結果。

替換 updateSearchResults(for:) 方法中的TODOfilterContentForSearchText(_:scope:)方法呼叫:

filterContentForSearchText(searchController.searchBar.text!)
複製程式碼

現在,不管使用者何時增加或者刪除搜尋框中的文字,UISearchController將通知MasterViewController,通過updateSearchResults(for:)方法。在其中呼叫filterContentForSearchText(_:scope:)對搜尋結果進行過濾。

編譯然後執行 App 現在,滾動到下面,你將看到在搜尋框下面的列表。

[譯]UISearchController 教程:開始使用

當輸入搜尋文字時,你什麼返回結果也看不到。這是因為你沒有寫處理返回資料的程式碼給 TableView。

更新 TableView

回到 MasterViewController.swift,增加一個方法在過濾時候呼叫:

func isFiltering() -> Bool {
  return searchController.isActive && !searchBarIsEmpty()
}
複製程式碼

替換tableView(_:numberOfRowsInSection:)方法:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  if isFiltering() {
    return filteredCandies.count
  }
    
  return candies.count
}
複製程式碼

這裡沒有做很多變動;簡單的檢查使用者是否在搜尋狀態下,然後決定使用正常資料來源或者搜尋的資料來源並更新 tableView。

接下來,替換tableView(_:cellForRowAt:)

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
  let candy: Candy
  if isFiltering() {
    candy = filteredCandies[indexPath.row]
  } else {
    candy = candies[indexPath.row]
  }
  cell.textLabel!.text = candy.name
  cell.detailTextLabel!.text = candy.category
  return cell
}
複製程式碼

這兩個方法都使用了 isFiltering(),來決定載入哪個資料來源。

當使用者點選搜尋框時候,active屬性自動被設定為true。然後從 filteredCandies 陣列載入資料。正常的時候是載入完整資料的。

回顧search controller的展示結果的處理過程,我們所做的只是根據狀態提供正確的資料來源。

此時編譯並執行 App。現在已經可以過濾 SearchBar 的搜尋內容啦!

[譯]UISearchController 教程:開始使用

雖然現在列表的內容顯示正確,但詳情頁展示的資料有誤。我們這就來修復它。

傳遞資料給詳情檢視

當想要傳遞資料給詳情檢視控制器,你需要確保檢視控制器知道使用者是從哪個檢視控制器進行的操作:所有資料列表,還是搜尋返回結果列表。仍然在MasterViewController.swift,在prepare(for:sender:),找到以下程式碼:

let candy = candies[indexPath.row]
Then replace it with the following:
let candy: Candy
if isFiltering() {
  candy = filteredCandies[indexPath.row]
} else {
  candy = candies[indexPath.row]
}
複製程式碼

這裡執行相同的isFiltering() 方法進行過濾。當使用者執行操作的時候,你需要提供正確的 Candy 傳遞給詳情檢視控制器。

此時編譯並執行程式碼,不管使用者是從資料列表檢視還是從搜尋結果檢視操作,App 都可以正確的導航至詳情檢視了。

[譯]UISearchController 教程:開始使用

建立一個範圍欄過濾返回結果

如果你想給使用者提供另一種過濾返回結果的方式,你可以新增一個範圍欄結合搜尋欄對搜尋結果進行分類。這裡分類的依據是甜品的種類:Chocolate,Hard,以及 Other。首先你必須在MasterViewController 中建立一個範圍欄。範圍欄是一個分段控制元件,通過它限制搜尋範圍。範圍是你所定義的。這裡是甜品的種類。範圍是可以自定義的,你可以使用型別、範圍或者其他完全不同的東西。使用範圍欄就像實現一個代理方法一樣容易。

MasterViewController.swift 中,你需要增加實現了UISearchBarDelegate協議的擴充套件:

extension MasterViewController: UISearchBarDelegate {
  // MARK: - UISearchBar Delegate
  func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
    filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])
  }
}
複製程式碼

當使用者切換範圍欄上的不同分類時,該方法會被呼叫。這時你需要呼叫filterContentForSearchText(_:scope:)

現在修改filterContentForSearchText(_:scope:)方法,將範圍考慮在裡面:

func filterContentForSearchText(_ searchText: String, scope: String = "All") {
  filteredCandies = candies.filter({( candy : Candy) -> Bool in
    let doesCategoryMatch = (scope == "All") || (candy.category == scope)
      
    if searchBarIsEmpty() {
      return doesCategoryMatch
    } else {
      return doesCategoryMatch && candy.name.lowercased().contains(searchText.lowercased())
    }
  })
  tableView.reloadData()
}
複製程式碼

現在的過濾邏輯是先匹配的分類,然後過濾掉所有名字不包含使用者輸入到搜尋框文字的物件。現在更新 isFilteing() 方法,以適配範圍欄被選擇時返回正確的結果

func isFiltering() -> Bool {
  let searchBarScopeIsFiltering = searchController.searchBar.selectedScopeButtonIndex != 0
  return searchController.isActive && (!searchBarIsEmpty() || searchBarScopeIsFiltering)
}
複製程式碼

已經接近成功了,但範圍過濾機制還不能很好的工作。還需要修改擴充套件中的updateSearchResults(for:),傳遞選擇的分類:

func updateSearchResults(for searchController: UISearchController) {
  let searchBar = searchController.searchBar
  let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
  filterContentForSearchText(searchController.searchBar.text!, scope: scope)
}
複製程式碼

最後一個問題是使用者還不能看到範圍欄。我們把目光移動到search controller初始化的地方。在MasterViewController.swiftviewDidLoad()方法中,新增以下程式碼在生成 candies 陣列之前:

// Setup the Scope Bar
searchController.searchBar.scopeButtonTitles = ["All", "Chocolate", "Hard", "Other"]
searchController.searchBar.delegate = self
複製程式碼

這會給搜尋條新增範圍欄,範圍欄中的標題來自甜品的分類。同樣的,包括一個「所有」分類,選擇這個分類,在搜尋的時候,顯示全部的分類內容。現在當你在搜尋框輸入搜尋文字,返回結果會包括所選擇的分類。

編譯並執行 App,輸入一些搜尋文字,然後切換範圍試試看。

增加一個指示器

為了解決這一問題,我們將新增一個底部檢視到我們的頁面中。當過濾狀態下它將顯示,並告訴使用者關於搜尋結果的資訊。開啟 SearchFooter.swift。這就是我們要用的底部檢視,它包含一個 Label 和介面。

回到MasterViewController.swift。你已經設定了底部檢視的IBOutlet,它在 Main.storyboard檔案中。接下來在 viewDidLoad() 方法中設定它:

// Setup the search footer
tableView.tableFooterView = searchFooter
複製程式碼

這將自定義 tableView 的底部檢視。接下來,你需要更新它的資訊在使用者執行搜尋的時候。替換 tableView(_:numberOfRowsInSection:) 方法的程式碼:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  if isFiltering() {
    searchFooter.setIsFilteringToShow(filteredItemCount: filteredCandies.count, of: candies.count)
    return filteredCandies.count
  }
    
  searchFooter.setNotFiltering()
  return candies.count
}
複製程式碼

到此,底部檢視新增完畢。

編譯並執行App,執行搜尋,觀察底部資訊的更新。點選鍵盤上的「搜尋」,隱藏鍵盤然後可以看到底部檢視。

[譯]UISearchController 教程:開始使用

[譯]UISearchController 教程:開始使用

相關文章