这篇教程是由iOS教程组的Nicolas Martin编写的。Nicolas是nmappworks的一名*iOS开发者。
在移动应用程序的世界里,用户对信息获取的速度要求非常高!
iOS用户希望他们需要的信息能够迅速地,直观地展现在他们面前。
因为UITableView的上下滚动能让用户迅速,自然地浏览大量信息,许多基于UIKit的应用都使用了UITableView来组织信息。但如果信息量非常非常大,让用户上下滚动如此长的列表是非常没有效率的。所以一个搜索的功能就是必须的了。
幸运的是,UIKit里有一个叫做UISearchBar的组件,能让用户迅速的筛选有用的信息。
在这个教程里,你会学习如何将这个搜索的组件融入你应用中的UITableView,包括加入自选的范围栏,使得你的应用更好用!
别被搜索栏吓到
今时今日,用户在应用里看到很长的列表时,都会期待一个搜索的功能。如果他们找不到搜索功能,他们会非常沮丧的!
问题是UISearchBar并不是那么容易用。UISearchBar的文档并不全面,这对第一次使用UISearchBar的开发者可能会比较有挑战。
但是,一但你对搜索栏有一定了解,特别是在读完这篇教程之后,你就不会觉得它有多难了。
其实UISearchBar挺懒的。它本身不做任何搜索。UISearchBar会提供一个基本的iOS搜索栏界面。它就像一个中等*一样,最在行的就是让别人做事情。(像我以前的老板一样!)
UISearchBar类用delegate协议的方式来告诉app的其他部分用户正在搜索栏中做什么。你需要自己编写对比字符串和过滤搜索的函数。这一听起来有点吓人,但是自定义的搜索功能让你对搜索过滤有更多的控制。你能够根据自己app的特点来修改搜索结果,让你的用户体验更迅速和智能的搜索。
在这个教程中,你将编写一个基于table view的有搜索功能的糖果app。(是的,好吃的糖果!)
首先,我们看看这个教程的概要:
- Candy 类: 为了让搜索函数明白如何过滤一些样本数据,你将会创建一个自定义的对象
- Table View视图: 我们会简单介绍如何创建一个table view视图。如果你已经熟悉这么做,那你可以很快的浏览这一部分。
- 搜索栏: 你将会在视图控制器中加入一个搜索栏的对象,这样才能进行搜索。
- 过滤数据队列: 你会学习如何使用一个能过滤的数据队列来处理搜索请求。
- 传送数据: 当你有搜索栏时,app的视图变化会需要传递相应的搜索数据。这一段就是为了巩固这类的知识的
- 范围栏: UISearchBar类还有一个强大的功能:让用户选择搜索的范围,以便于进一步缩小搜索结果。
- 隐藏UISearchBar: 最后一部分你将学到如何在用户不需要搜索时隐藏搜索栏!
准备好做糖果搜索了吗?那我们开始吧!
我想要吃糖果
在XCode里,点击”File New Project…”然后选择”iOS Application Single View Application”。将project命名为”CandySearch”。请确保在”Use Storyboards”和”Use Automatic Reference Counting”的选项旁打钩。最后,确保device栏中你的选择是iPhone,然后将这个project保存到你认为合适的路径。
我们先清理掉一些默认加入的文件,这样我们才能从零开始。在XCode左边的Project Navigator中多选ViewController.h和ViewController.m,点击鼠标右键打开菜单,选择Delete,然后点击”Move to Trash”(移放到垃圾桶)。点击打开MainStoryboard.storyboard,选中里面唯一的view controller(视图控制器)然后删除它。
现在我们开始重新按自己的要求来构建这个project。首先从storyboard开始。从位于XCode右侧栏下方的Object Browser(对象浏览器)中拖出一个Navigation Controller(导航控制器)到storyboard(故事版)中。这会生成两个view,一个代表那个navigation controller(导航控制器)本身,另一个是UITableView。这个UITableView将成为我们程序的第一个用户看见的视图。
最后还需要加入一个视图控制器来控制当用户搜索列表时显示的详细内容视图。将一个View Controller对象拖入故事版(storyboard)中。你需要将这个视图控制器与UITableView的视图控制器联系起来。按住control键然后从Table View点击拖动至那个新的视图控制器,在弹出的manual segue窗口中选择“Push”做为视图替换的模式。现在,你的project应该和下图类似:
创建Candy类
下面你将创建一个数据模型类来保存每种糖果的类型和名字等信息。
创建一个新的文件并使用“iOS Cocoa Touch Objective-C class”模板。将这个类命名为Candy,并将它设为NSObject的子类。
打开Candy.h文件并将其内容改为如下:
#import <Foundation/Foundation.h> @interface Candy : NSObject { NSString *category; NSString *name; } @property (nonatomic, copy) NSString *category; @property (nonatomic, copy) NSString *name; + (id)candyOfCategory:(NSString*)category name:(NSString*)name; @end |
这个类的对象有两个property(属性),一个是糖果的种类,另一个是糖果的名字。当用户搜索糖果时,你会用糖果的名称和用户输入的字符串进行比较。而糖果的种类只有在教程后面使用范围搜索时才会有用。
将Candy.m文件的内容改为如下:
#import "Candy.h" @implementation Candy @synthesize category; @synthesize name; + (id)candyOfCategory:(NSString *)category name:(NSString *)name { Candy *newCandy = [[self alloc] init]; newCandy.category = category; newCandy.name = name; return newCandy; } @end |
上面加入的方法是用来初始化你的Candy类对象的。这个方法需要一个种类和名称的字符串。你将会将这些Candy对象展示在列表中,让用户可以轻易使用你的搜索功能来过滤显示的内容。
现在我们可以开始设置UITableView了。
设置UITableViewController(列表视图控制器)/h2>
下面我们将设置一个UITableView。用“iOSCocoa TouchObjective-C class ”模板创建一个新的文件,命名为CandyTableViewController,并将其设为UITableViewController的子类。
我们先添加一个数组来储存数据。打开CandyTableViewController.h文件并在@interface一行下面加入:
@property (strong,nonatomic) NSArray *candyArray; |
我们对CandyTableViewController.m文件进行一些简单的修改,只为了先完成一个示范性的列表。首先,清理掉一些我们不需要的模板自带的代码。将“(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath”以及其之后一直到文件最后一段(除了@end)的代码全部删除。
同时删除numberOfSectionsInTableView:方法,因为我们不需要用到它。
在文件顶端,导入Candy.h头文件,让我们的控制器知道这个数据模型:
#import "Candy.h"
|
然后在“@implementation”下面加入如下代码:
@synthesize candyArray;
|
现在你的控制器已经知道了Candy类数据模型,所以你可以通过candyOfCategory:name:方法来加入一些示范数据。在这个教程里,你只需要创建为数不多的示范数据来展示搜索栏的功能。但是在真实的app中,可能会有成百上千的数据。但无论数据是多或少,我们的搜索都能用!
在viewDidLoad方法中,删除模板自带的备注并加入下面的代码来提供示范性数据:
// CandyArray 的示范数据 candyArray = [NSArray arrayWithObjects: [Candy candyOfCategory:@"chocolate" name:@"chocolate bar"], [Candy candyOfCategory:@"chocolate" name:@"chocolate chip"], [Candy candyOfCategory:@"chocolate" name:@"dark chocolate"], [Candy candyOfCategory:@"hard" name:@"lollipop"], [Candy candyOfCategory:@"hard" name:@"candy cane"], [Candy candyOfCategory:@"hard" name:@"jaw breaker"], [Candy candyOfCategory:@"other" name:@"caramel"], [Candy candyOfCategory:@"other" name:@"sour chew"], [Candy candyOfCategory:@"other" name:@"peanut butter cup"], [Candy candyOfCategory:@"other" name:@"gummi bear"], nil]; // 刷新tableView列表 [self.tableView reloadData]; |
将tableView:numberOfRowsInSection:的内容修改为如下:
// 返回candyArray数组的长度 return [candyArray count]; |
在最后一个“@end”前加入下面代码:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if ( cell == nil ) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // 创建一个Candy对象 Candy *candy = nil; candy = [candyArray objectAtIndex:indexPath.row]; // 设置列表的一行 cell.textLabel.text = candy.name; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; return cell; } |
首先,你告诉列表控制器每一行应该显示什么。然后从candyArray数组中通过indexPath来获取相应的Candy对象,然后用这个Candy对象的内容添入相应的行中在列表视图中显示出来。
你还需要在storyboard中做一些设置,上述的代码才会有效。打开MainStoryboard.storyboard文件,选择Root View Controller然后在Identity Inspector(右边设置栏上方的第三栏)中将它的类改为CandyTableViewController。
接着在左手工具栏点击选择table view,然后选择Connections Inspector(右边工具栏上方的第六栏)。将dataSource和delegate与Candy Table View Controller连接起来(点击dataSource和delegate旁的圆点然后拖至Candy Table View Controller)。
双击”Root View Controller”标题并将其改为”CandySearch”。
保存并编译运行。你的table view应该显示我们输入的示范数据!糖果太多,时间太少。我们需要一个搜索栏!
设置UISearchBar
现在是时候设置UISearchBar了!打开storyboard(故事版)文件然后将一个“Search Bar and Search Display Controller”对象拖入到table view controller中。注意,对象库中还有一个“Search Bar” 对象,那不是我们想要的。将搜索栏放在导航栏和Table View之间。
小贴士:不知道什么是“Search Display Controller”(搜索显示控制器)?根据苹果自己的文档记载,一个搜索显示控制器用来控制一个搜索栏以及一个table view。这个table view会显示搜索过滤后的信息。而这些信息来源于另一个视图控制器。也就是说,在我们的情况下,你的“search display controller”(搜索显示控制器)需要知道那个table view controller控制的那些示范数据。然后在搜索显示控制器自己的table view中显示搜索过滤后的结果。这个table view会覆盖table view controller的视图,这样用户只能看到过滤后的结果。
UISearchBar的属性设置
在storyboard界面下的Attributes inspector栏,请花点时间浏览一下Search Bar对象的各种属性。就算你不需要用到它们,但了解UIKit组件的各种属性也是有价值的。
- Text: 这会改变出现在搜索栏中的默认字符串。因为我们程序中的搜索栏不需要默认值,我们不需要添入任何字符。
- Placeholder: 在搜索栏没有添入任何字符串时,一般会显示一串灰色的字符来提示用户输入搜索信息。这个属性就是用来设置这个提示的内容的。在我们的糖果程序中,就显示”Search for Candy”(搜索糖果)好了。
- Prompt: 这个属性的值会出现在搜索栏上方。对于有复杂搜索功能的程序,用户可能需要一些指导信息。(但在我们的这个简单程序中,灰色的提示信息应该就足够了!)
- Style & Tint: 这些属性能让你改变搜索栏的格调和颜色。为了让你的设计看起来更和谐,我建议你用和UINavigationBar上这些属性同样的设置。
-
Show Search Results Button:
如果你在这个属性旁打钩,搜索栏右边就会出现一个灰色按钮。这个按钮可以用来显示最近几次的搜索,或者上次搜索的结果。这个按钮的的功能可以通过Search Bar Delegate内的方法来控制。
-
Show Bookmarks Button:
如果你在这个属性旁打钩,搜索栏右边就会出现一个标准的蓝色书签按钮。用户可以通过这个按钮调出他们储存的书签。这个按钮的行为同样通过Search Bar Delegate内的方法来控制。 -
Show Cancel Button:
如果你在这个属性旁打钩,搜索栏右边就会出现一个标准的取消按钮,让用户取消搜索。不要在这个属性旁打钩,因为当你在搜索栏中输入字符串后,这个按钮会自动出现。 -
Shows Scope Bar(显示范围栏) & Scope Titles(范围标题):
范围栏让用户可以进一步缩小搜索范围。比如在一个音乐程序中,这个范围栏可以让用户将搜索局限于艺术家名字,专辑或者音乐类型。现在先不要在这个属性旁打钩。你会在这个教程的后半部分设置这个范围栏。 -
Capitalize(大小写), Correction(自动纠错) & Keyboard(键盘):
这些属性是从UITextField中直接搬过来的,让你能改变搜索栏的一些行为。比如,如果用户需要搜索的是商店名称或者人名,那你应该将自动纠错关闭,否则用户输入的名字很有可能被自动纠错成为其他词语。在这个教程中我们糖果的名字都是字典里有的名词,所以你可以不用关闭自动纠错。
注意: 总是清楚地知道你有什么选择能提高开发效率。所以以后在iOS的开发中,你应该记得花时间看看所有有可能用到的选项设置。
设置UISearchBarDelegate
在完成storyboard的相关设置后,我们需要写一些代码来让搜索栏运作起来。打开CandyTableViewController.h文件并将:
@interface CandyTableViewController : UITableViewController |
改为:
@interface CandyTableViewController : UITableViewController <UISearchBarDelegate, UISearchDisplayDelegate> |
这里我们将theUISearchBarDelegate类和UISearchDisplayDelegate类加入到CandyTableViewController类中。
并且,加入一个搜索栏的IBOutlet以及一个叫做”filteredCandyArray”的数组来储存搜索的结果。
@property (strong,nonatomic) NSMutableArray *filteredCandyArray; @property IBOutlet UISearchBar *candySearchBar; |
别忘了在CandyTableViewController.m文件中“synthesize”这些新的属性:
@synthesize filteredCandyArray; @synthesize candySearchBar; |
现在你的代码中有一个outlet了,你需要将storyboard中的搜索栏和这个outlet连接起来。打开故事版文件,选择“Candy Table View”控制器,打开“Connections Inspector”栏然后从candySearchBar outlet拖一根线出来和搜索栏连接起来:
编译并运行,你应该能在模拟器中看到那个搜索栏,只是它似乎不工作!你需要使用那个filteredCandyArray数组。
设置filteredCandyArray数组
首先你需要初始化那个NSMutableArray。你可以将它的初始大小设为candyArray数组的大小,因为你不可能有比原来数组更大的过滤后的数组。将下面的代码加到CandyTableViewController.m文件的viewDidLoad方法中我们设置candyArray的地方,但是在[self.tableView reloadData]之前:
// 初始化filteredCandyArray数组,使它和candyArray数组的大小一样。 self.filteredCandyArray = [NSMutableArray arrayWithCapacity:[candyArray count]]; |
将下面的代码加入到文件的尾部:
#pragma mark Content Filtering -(void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope { // 根据搜索栏的内容和范围更新过滤后的数组。 // 先将过滤后的数组清空。 [self.filteredCandyArray removeAllObjects]; // 用NSPredicate来过滤数组。 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.name contains[c] %@",searchText]; filteredCandyArray = [NSMutableArray arrayWithArray:[candyArray filteredArrayUsingPredicate:predicate]]; } |
上面的方法会根据搜索内容来过滤candyArray数组,并将结果放置到清空后的filteredCandyArray数组中。在清空filteredCandyArray后,我们用NSPredicate来过滤candyArray数组。
NSPredicate可以根据一个简单的条件字符串来过滤一个数组。注意NSPredicate的格式:
@"SELF.name contains[c] %@",searchText |
看起来很复杂啊?别怕,这其实很简单的。SELF.name指的是数组中每一个Candy对象的“name”属性。”contains[c]“会让predicate搜索“name”属性中的字符串,看看有没有和后面提供的“searchText”一样的字符串。大小写有别。
接着,将下面的代码加到文件末端:
#pragma mark - UISearchDisplayController Delegate Methods -(BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString { // 当用户改变搜索字符串时,让列表的数据来源重新加载数据 [self filterContentForSearchText:searchString scope: [[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:[self.searchDisplayController.searchBar selectedScopeButtonIndex]]]; // 返回YES,让table view重新加载。 return YES; } -(BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption { // 当用户改变搜索范围时,让列表的数据来源重新加载数据 [self filterContentForSearchText:self.searchDisplayController.searchBar.text scope: [[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:searchOption]]; // 返回YES,让table view重新加载。 return YES; } |
上面的代码使得UISearchDisplayController Delegate的两个方法在用户改变搜索内容时会去调用实际过滤内容的函数(filterContentForSearchText)。
第一个方法在用户改变搜索栏里的字符串时就会被调用。第二个方法则在范围栏改变时被调用。你还没有在app中添加这个范围栏,所以这个方法只有在教程后面加入范围栏后才会有用。
编译并运行你的程序;你会发现搜索栏还是不能用,怎么回事呢?这是因为你还没有编写代码来让cellRowForIndexPath:方法知道什么时候应该用一般的数据,什么时候用过滤后的数据。将:
candy = [candyArray objectAtIndex:[indexPath row]]; |
改为:
// 检查现在应该显示普通列表还是过滤后的列表 if (tableView == self.searchDisplayController.searchResultsTableView) { candy = [filteredCandyArray objectAtIndex:indexPath.row]; } else { candy = [candyArray objectAtIndex:indexPath.row]; } |
上面的代码检查现在现实的列表视图属于普通列表还是搜索后的列表。如果是搜索列表的话,视图中实际显示的数据应该来自filteredCandyArray。否则,数据应该来自那个完整的数组。Display Controller会自动负责显示和隐藏相应的列表,所以我们只需要提供正确的(过滤或者没有过滤过的)数据,列表视图就会正确地显示出来。
numbersOfRowsInSection:方法也需要一些修改,因为过滤后的数组的长度显然和没过滤过的数组不一样。将numbersOfRowsInSection:的内容改为如下:
// 检查现在显示的是哪个列表视图,然后返回相应的数组长度 if (tableView == self.searchDisplayController.searchResultsTableView) { return [filteredCandyArray count]; } else { return [candyArray count]; } |
编译并运行程序。你的搜索栏已经可以使用了!不信你可以搜索一下你想要的糖果名字。
注意: 你或许也注意到numberOfRowsInSection:方法中用来检查现在显示的列表视图的if/else逻辑在很多其他地方也需要。忘了做这个检查或导致奇怪的bug。你只需要记住搜索过的内容会显示在一个另外的列表视图中。两个列表视图完全是分开的两个视图,苹果的设计让用户难以察觉这个区别。所以导致许多开发者的误解。
将信息发送到详细视图(detail view)
类似刚才的情况,在详细视图中,详细视图控制器也需要知道现在使用的是哪一个列表视图:有所有内容的视图,或是有过滤后内容的视图。将下面的代码加入到CandyTableViewController.m文件中:
#pragma mark - TableView Delegate -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // 切入详细视图 [self performSegueWithIdentifier:@"candyDetail" sender:tableView]; } #pragma mark - Segue -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([[segue identifier] isEqualToString:@"candyDetail"]) { UIViewController *candyDetailViewController = [segue destinationViewController]; // 我们需要知道哪个是现在正显示的列表视图,这样才能从相应的数组中提取正确的信息,显示在详细视图中。 if(sender == self.searchDisplayController.searchResultsTableView) { NSIndexPath *indexPath = [self.searchDisplayController.searchResultsTableView indexPathForSelectedRow]; NSString *destinationTitle = [[filteredCandyArray objectAtIndex:[indexPath row]] name]; [candyDetailViewController setTitle:destinationTitle]; } else { NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow]; NSString *destinationTitle = [[candyArray objectAtIndex:[indexPath row]] name]; [candyDetailViewController setTitle:destinationTitle]; } } } |
打开storyboard并确保连接糖果列表视图控制器与详细视图控制器的segue的identifier是”candyDetail”。编译并运行程序,当你点击列表视图中的任意一行时,那个糖果的详细信息就会显示在切入的详细视图中。
使用范围搜索
我们还可以通过糖果的种类来进一步缩小搜索范围。我们的糖果数据总共有三类(巧克力,硬糖和其他)。
首先,在storyboard中加入一个范围栏。来到CandySearch视图控制器并点击选择搜索栏。在 attributes inspector(属性设置栏)中,在”Shows Scope Bar”选项旁打勾。然后将范围栏的标题改为:”All”, “Chocolate”, “Hard” ,和 “Other”。(你可以点击+按钮来加入更多的种类,双击来修改标题)。你的屏幕应该和下图类似:
下面我们需要修改CandyTableViewController.m文件中的filterContentForSearchText:方法,让它在过滤搜索时也会考虑选中的类别:
-(void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope { // 根据搜索栏内的字符串以及搜索范围来过滤数据。 [self.filteredCandyArray removeAllObjects]; // 用NSPredicate来过滤数组。 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.name contains[c] %@",searchText]; NSArray *tempArray = [candyArray filteredArrayUsingPredicate:predicate]; if (![scope isEqualToString:@"All"]) { // 进一步用糖果类别来过滤数据 NSPredicate *scopePredicate = [NSPredicate predicateWithFormat:@"SELF.category contains[c] %@",scope]; tempArray = [tempArray filteredArrayUsingPredicate:scopePredicate]; } filteredCandyArray = [NSMutableArray arrayWithArray:tempArray]; } |
上面的代码使用了一个新的NSPredicate,叫做scopePredicate。如果选中的类别为“All”,我们则不做任何进一步的过滤。
编译并运行程序。范围栏应该已经出现在视图里了。但是我们在没有搜索时是不应该看到这个范围栏的。将下面的代码加入到viewDidLoad方法的顶端(就则调用super之后):
// 在用户使用搜索栏前隐藏范围栏。 [candySearchBar setShowsScopeBar:NO]; [candySearchBar sizeToFit]; |
编译并运行。现在,你的范围栏应该和下图类似:
像音乐app中那样隐藏UISearchBar
如果你注意苹果自带的音乐app,那里面音乐列表的搜索栏一开始是隐藏起来的。这样你的列表试图就会有更多的空间。在viewDidLoad方法中,你刚才加入代码的下方加入下面的代码:
// 将搜索栏藏起来(搜索栏只有在用户滚动到列表视图顶端时才会出现) CGRect newBounds = self.tableView.bounds; newBounds.origin.y = newBounds.origin.y + candySearchBar.bounds.size.height; self.tableView.bounds = newBounds; |
因为你的搜索栏一开始是隐藏起来的,我们应该加入一个按钮来让用户知道列表视图有搜索功能。
首先,我们应该加入一个IBAction。 将下面的代码加入到CandyTableViewController.m文件的低端:
-(IBAction)goToSearch:(id)sender { // 如果你担心用户无法发现藏在列表顶端的搜索栏,那我们在导航栏加一个搜索图标。 // 如果你不隐藏搜索栏,那就别加入这个搜索图标,否则就重复了。 [candySearchBar becomeFirstResponder]; } |
将下面的方法定义加入到CandyTableViewController.h文件:
-(IBAction)goToSearch:(id)sender; |
现在打开storyboard,在导航栏加一个Bar Button Item(栏按钮)。在Attributes Inspector(属性设置栏),将identifier的值改为“search”,这样那个按钮就会用苹果默认的放大镜做为图标。然后在connections inspector(链接设置栏),将那个按钮和goToSearch:方法联系起来。
这样就好了!你的“Search”按钮应该能用了。编译并运行:
如果看到和上图类似的界面,你已经大功告成了!