一個常見的問題是我如何在UITableView里用一個搜索欄實現數據搜索。 本章節將展示如何往標簽欄項目添加一個搜索欄 。 有了搜索欄,程序允許用戶通過指定一個搜索詞搜索菜譜列表。
嗯,添加一個搜索欄不是很難,但這需要一點額外的工作。 我們將繼續從前一教程Xcode項目中開發的程序做基礎 。 如果你沒有經歷過前面的教程,花些時間來看看 。
理解搜索顯示控制器(Search Display Controller)
您可以使用搜索顯示控制器(例如UISearchDisplayController類)來管理你的應用程序搜索。一個搜索顯示控制器包括一個搜索欄和顯示搜索結果表視圖。
當用戶啟動一個搜索,搜索顯示控制器將在原始視圖上添加搜索界面和顯示搜索結果界面。 有趣的是,結果顯示在搜索顯示控制器創建的表視圖中。

搜索結果表視圖和表視圖
像其他視圖控制器一樣,您可以以編程方式創建搜索顯示控制器或簡單地把它添加到您的使用故事版的應用程序。 在本教程中,我們將使用后一種方法。
在故事板添加一個搜索顯示控制器
在故事板中、從食譜視圖控制器右下方的導航欄拖拽出“搜索欄和搜索顯示控制器(Search Bar and Search Display Controller)”。 如果你做得對,你應該有一個類似於如下的樣子:

添加搜索顯示控制器
在繼續之前,嘗試運行這個應用程序,看看它的樣子。在 沒有添加任何新的代碼之前,您已經有一個搜索欄。 點擊搜索欄將把你帶到搜索界面。 然而,搜索不會給你正確的搜索結果。

搜索欄出現在表視圖應用程序但是不工作
我們什么也沒做但為什么搜索結果顯示所有的食譜嗎?
正如前面提到的,搜索結果顯示在搜索顯示控制器創建的表視圖中。 當繼續開發 表視圖應用程序的時候 之前,我們實現需要通過UITableViewDataSource協議告訴表視圖有多少行數據,每一行中的數據是什么。
像UITableView一樣,表視圖創建的搜索顯示控制器也要采用相同的方法。 根據 UISearchDisplayController的官方文檔 介紹,下面是可用的委托,能讓你控制搜索結果和搜索欄:
搜索結果表視圖的數據源(data source)。
這個對象是負責提供的數據結果表的數據源的。搜索結果表視圖的委托(delegate)。
這個對象是負責在其他的事情中,響應用戶的選擇一個項目在結果表。搜索顯示控制器的委托。
委托符合UISearchDisplayDelegate協議。 當搜索的開始或結束,當搜索界面顯示或隱藏時通知我們。 方便的是,它也可以告知是否改變搜索字符串或搜索范圍,以便結果表視圖可以被重新加載。搜索欄的委托。
這個對象是負責對搜索標准的變化進行響應。
通常,原始的視圖控制器作為搜索結果的數據來源和委托的源對象。 你不需要手動連接數據源、委托與視圖控制器。 當你插入搜索欄到食譜視圖控制器的視圖中時,和搜索結果數據來源、委托的連接已經自動完成了。 你可以按下“控制”鍵,點擊“搜索顯示控制器”,會出現一個彈窗,揭示了連接。

搜索顯示控制器的連接
兩個表視圖(即在食譜視圖控制器的表視圖和搜索結果表視圖)共享同一個視圖控制器來處理數據的進入。 如果你跳到代碼,顯示表數據時有兩個方法被調用:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
-
(NSInteger
)tableView
:
(UITableView
*
)tableView numberOfRowsInSection
:
(NSInteger
)section
{ return [recipes count ]; } - (UITableViewCell * )tableView : (UITableView * )tableView cellForRowAtIndexPath : ( NSIndexPath * )indexPath { static NSString *simpleTableIdentifier = @ "RecipeCell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier :simpleTableIdentifier ]; if (cell == nil ) { cell = [ [UITableViewCell alloc ] initWithStyle :UITableViewCellStyleDefault reuseIdentifier :simpleTableIdentifier ]; } cell.textLabel.text = [recipes objectAtIndex :indexPath.row ]; return cell; } |
這就解釋了為什么搜索結果會顯示的完整列表食譜,而不管你的搜索詞。
實現搜索過濾器
顯然,為了使搜索正常工作,有幾件事情我們必須實現/改變:
- 實現方法來篩選菜單名稱並返回正確的搜索結果
- 改變數據源方法來區分表視圖。 如果tableView傳遞是食譜書視圖控制器的表視圖,我們顯示所有的食譜。 另一方面,如果它是一個搜索結果表視圖,只顯示搜索結果。
首先,我們將向您展示如何實現過濾器。 這里我們有一個數組來存儲所有的食譜。 我們創建另一個數組來保存搜索結果。 我們的名字是“searchResults”。
接下來,添加一個新的方法來處理搜索過濾。 過濾在iOS應用是一個常見的任務。最直接的辦法通過遍歷菜單數組並逐一進行判斷來過濾。 這樣的實現沒有錯。 但iOS SDK提供了名為Predicate(謂語)的更好的方法來處理搜索查詢。 通過使用NSPredicate(這是一個Predicate(謂語)的對象表示),您可以編寫更少的代碼。 只需兩行代碼,它會搜索所有的食譜並返回通過匹配的結果。
1
2 3 4 5 6 7 8 |
-
(
void
)filterContentForSearchText
:
(
NSString
*
)searchText scope
:
(
NSString
*
)scope
{ NSPredicate *resultPredicate = [ NSPredicate predicateWithFormat : @ "SELF contains[cd] %@", searchText ]; searchResults = [recipes filteredArrayUsingPredicate :resultPredicate ]; } |
基本上,一個predicate(謂詞)是一個表達式,返回一個布爾值(真或假)。 你指定搜索條件格式的NSPredicate並用它來過濾裝載數據的數組。 NSArray提供filteredArrayUsingPredicate:方法返回一個新的包含匹配Predicate對象的數組。 self關鍵字指自己,“self contains[cd]% @”指的是自己包含某一特定的值。 操作”[cd]”意味着比較事件。
實現搜索顯示控制器的委托
現在我們已經創建了一個方法來處理數據過濾。 但它什么時候被調用呢? 顯然,當用戶在輸入關鍵字是,filterContentForSearchText:方法被調用。 這個UISearchDisplayController類帶有一個shouldReloadTableForSearchString:方法,將會在它每次搜索字符串變化時自動調用。 所以添加以下方法在RecipeBookViewController.m:
1
2 3 4 5 6 7 8 9 10 |
-
(
BOOL
)searchDisplayController
:
(UISearchDisplayController
*
)controller
shouldReloadTableForSearchString : ( NSString * )searchString { [self filterContentForSearchText :searchString scope : [ [self.searchDisplayController.searchBar scopeButtonTitles ] objectAtIndex : [self.searchDisplayController.searchBar selectedScopeButtonIndex ] ] ]; return YES; } |
在searchResultsTableView顯示搜索結果
如前所述,我們必須改變數據源方法來區分表視圖(即在食譜書視圖控制器的表視圖和搜索結果表視圖)。 這很容易區分表視圖。 我們只需簡單地比較了tableView對象與searchDisplayController”的“searchResultsTableView”。如果比較是相同的,我們顯示搜索結果,而不是所有的食譜。 這里有兩種方法的變化:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
-
(NSInteger
)tableView
:
(UITableView
*
)tableView numberOfRowsInSection
:
(NSInteger
)section
{ if (tableView == self.searchDisplayController.searchResultsTableView ) { return [searchResults count ]; } else { return [recipes count ]; } } - (UITableViewCell * )tableView : (UITableView * )tableView cellForRowAtIndexPath : ( NSIndexPath * )indexPath { static NSString *simpleTableIdentifier = @ "RecipeCell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier :simpleTableIdentifier ]; if (cell == nil ) { cell = [ [UITableViewCell alloc ] initWithStyle :UITableViewCellStyleDefault reuseIdentifier :simpleTableIdentifier ]; } if (tableView == self.searchDisplayController.searchResultsTableView ) { cell.textLabel.text = [searchResults objectAtIndex :indexPath.row ]; } else { cell.textLabel.text = [recipes objectAtIndex :indexPath.row ]; } return cell; } |
再次測試應用程序
一旦你完成上述變化,測試您的應用程序再次。 現在的搜索欄應該正常工作!
處理在搜索結果中的行選擇
盡管搜索框工作了,但它不會回應你的行選擇。 我們想讓它像菜單表視圖一樣工作。 當用戶點擊任何搜索結果,它將跳轉到詳細視圖顯示選擇的菜單名稱。
早些時候,我們使用聯線聯系表格單元的菜單和詳細視圖。 (如果你忘了這事做得怎么樣了,重新復習 以前的教程看到它是如何工作的。)
顯然,我們必須在故事板創建另一個聯線來定義搜索結果和詳細視圖之間的過渡。 問題是我們不能那樣做。 搜索結果表視圖是搜索顯示控制器的一個私有變量。 使用故事板處理搜索結果的行選擇是不可能的。
然而,搜索顯示表視圖提供了一個委托讓你操作結果表。 當用戶選擇一行,didSelectRowAtIndexPath:方法將調用。 因此,我們要做的就是實現該方法:
1
2 3 4 5 6 |
-
(
void
)tableView
:
(UITableView
*
)tableView didSelectRowAtIndexPath
:
(
NSIndexPath
*
)indexPath
{ if (tableView == self.searchDisplayController.searchResultsTableView ) { [self performSegueWithIdentifier : @ "showRecipeDetail" sender : self ]; } } |
我們簡單地調用performSegueWithIdentifier:方法手動觸發“showRecipeDetail”標識的聯線。 在繼續編碼之前,再次嘗試運行這個應用程序。 當您選擇的任何搜索結果,應用程序顯示了詳細視圖與一個菜單名稱。 但這個名字並不總是正確的。
回到prepareForSegue:方法,我們使用“indexPathForSelectedRow”方法來檢索indexPath所選擇的行。 正如前面提到的,搜索結果會顯示在一個單獨的表視圖。 但在我們原先的prepareForSegue:方法中,我們總是檢索從表視圖的食譜視圖控制器選中的行。 這就是為什么我們在細節視圖有錯誤的菜單名稱。 做出正確的選擇行的搜索結果,我們必須調整prepareForSegue:方法:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
-
(
void
)prepareForSegue
:
(UIStoryboardSegue
*
)segue sender
:
(
id
)sender
{
if ( [segue.identifier isEqualToString : @ "showRecipeDetail" ] ) { RecipeDetailViewController *destViewController = segue.destinationViewController; NSIndexPath *indexPath = nil; if ( [self.searchDisplayController isActive ] ) { indexPath = [self.searchDisplayController.searchResultsTableView indexPathForSelectedRow ]; destViewController.recipeName = [searchResults objectAtIndex :indexPath.row ]; } else { indexPath = [self.tableView indexPathForSelectedRow ]; destViewController.recipeName = [recipes objectAtIndex :indexPath.row ]; } } } |
我們首先確定用戶是否使用搜索功能。 當用戶使用搜索功能的話,我們從searchResultsTableView得到indexPath。 否則,我們就從表視圖的食譜視圖控制器得到indexPath。
就是這樣。 再次運行應用程序,搜索選擇應該按預期的那樣工作。