一个常见的问题是我如何在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。
就是这样。 再次运行应用程序,搜索选择应该按预期的那样工作。