UISearchController
是 iOS 8 之后推出的用于管理搜索事件的控件, 在使用该控件的过程中要注意很多坑,下面我将带领大家来一步步的学习该如何使用该控件,并且最后带领大家来模仿一下京东首页的搜索框以及美团的地址搜索框,具体如下图:
废话不多说 我们开始
基本使用
iOS 11.0 之前版本
1. 结果控制器 是nil
定义 tableView
和 searchController
两个属性,以及遵守相关的协议:
@interface GSNormalSearchVC ()<UITableViewDelegate,UITableViewDataSource,UISearchResultsUpdating,UISearchControllerDelegate>{
NSArray *_dataArray;//数据源
NSArray *_filterArray;
}
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) UISearchController *searchController;
对这两个属性懒加载:
#pragma mark --getter method
-(UISearchController *)searchController{
if (!_searchController) {
//创建UISearchController,当ResultsController为nil的时候当前控制器就是结果控制器
_searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
_searchController.hidesNavigationBarDuringPresentation = YES;//当搜索框激活时, 是否隐藏导航条 default is YES;
_searchController.dimsBackgroundDuringPresentation = YES;//当搜索框激活时, 是否添加一个透明视图 default is YES
_searchController.searchBar.placeholder = @"查找您所在的区域";//搜索框占位符
_searchController.delegate = self;
_searchController.searchResultsUpdater = self;
}
return _searchController;
}
-(UITableView *)tableView{
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.tableFooterView = [[UIView alloc] init];
_tableView.tableHeaderView = self.searchController.searchBar;
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellID];
}
return _tableView;
}
实现遵守的协议:
#pragma mark --UISearchResultsUpdating
-(void)updateSearchResultsForSearchController:(UISearchController *)searchController{
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(SELF CONTAINS %@)",searchController.searchBar.text];
_filterArray = [_dataArray filteredArrayUsingPredicate:predicate];
_filterArray = searchController.searchBar.text.length > 0 ? _filterArray : _dataArray;
[self.tableView reloadData];
}
#pragma mark --UISearchControllerDelegate
-(void)willPresentSearchController:(UISearchController *)searchController{
NSLog(@"将要弹出searchController");
}
-(void)willDismissSearchController:(UISearchController *)searchController{
NSLog(@"将要消失searchController");
}
#pragma mark --UITableViewDataSource
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return _filterArray.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellID forIndexPath:indexPath];
cell.textLabel.text = _filterArray[indexPath.row];
return cell;
}
效果图大致如下:
如果此时我们尝试着将 automaticallyAdjustsScrollViewInsets
设置为 NO
的时候,我们可以看到当搜索控件处于激活状态的时候 SearchBar
和底部内容之间的间距突然就拉大了,具体如下图所示:
所以这也告诉我们一个问题 在使用 UISearchController
的时候切记不要轻易的 改变 automaticallyAdjustsScrollViewInsets
内部结构
通过 Debug View Hierarchy
来看看此时它的内部结构是怎样的.
当 UISearchController 未处于激活状态
下的结构如下图所示:
当处于激活状态下
,通过下图我们可以看到多了几个View: UIDimmingView
UISearchBarContainerView
通过了解 UISearchController
的内部结构有助于下文我们自定义一个属于我们自己的搜索控件 , 具体细节 后文会详细介绍
自定义 结果控制器
如果我们想自定义一个 结果控制器 我们可以在创建 UISearchController
的时候直接指定即可:
GSSearchResultVC *resultVC = [[GSSearchResultVC alloc] init];
_searchController = [[UISearchController alloc] initWithSearchResultsController:resultVC];
然后在 updateSearchResultsForSearchController:
中将搜索到的结果传递给结果控制器即可:
#pragma mark --UISearchResultsUpdating
-(void)updateSearchResultsForSearchController:(UISearchController *)searchController{
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(SELF CONTAINS %@)",searchController.searchBar.text];
_filterArray = [_dataArray filteredArrayUsingPredicate:predicate];
_filterArray = searchController.searchBar.text.length > 0 ? _filterArray : _dataArray;
GSSearchResultVC *resultVC = (GSSearchResultVC *) searchController.searchResultsController;
resultVC.filterDataArray = _filterArray;
// [self.tableView reloadData];
}
运行效果大致如图所示:
此时我们便遇到了使用过程中的第一个坑 相信细心的朋友已经发现了:当搜索框处于激活状态的时候 SearchBar和搜索到的结果之间的距离越来越大.
而且当UISearchController
处于active
状态下用户push到下一个控制器的时候 SearchBar 却仍然仍留在界面上
那么该如何解决上述问题呢?
此时我们只需要将 self.definesPresentationContext = YES;
即可,添加该行代码之后运行起来一切正常.
内部结构图
跟上面的非常类似,只是当 UISearchController
处于激活状态的时候,我们看到 UISearchControllerView
中多了一个 UIView,该View其实就是 resultViewController.view,如下图所示:
注意
在使用UISearchController
的时候, 我们需要将其设置为全局变量或者控制器属性, 使其生命周期与控制器相同; 如果设置为局部变量, 则会提前销毁, 导致无法使用.
iOS 11.0 的新变化
我们将上述代码在 iOS 11 的环境下运行起来以后 如下图所示:
上图是在iOS 11 环境下的一点小变化, 我相信细心的朋友已经发现问题了, 当搜索框处于
active
的状态的时候 默认情况下 第一行和 searchBar
之间的间距变大了,我们通过 Debug View Hierarchy
来看看结构发生了什么变化,为什么会这样呢?
通过上图我们发现在 iOS 11
环境中 , 激活状态下 此时 UISearchBar
已经不是添加 到 UISearchControllerView
中 而是被添加到了 导航栏中了,所以为了解决这个问题 我们需要单独的适配iOS 11
if (@available(iOS 11, *)) {
self.navigationItem.searchController = self.searchCtrl;
self.navigationItem.hidesSearchBarWhenScrolling = NO;
} else {
self.tableView.tableHeaderView = self.searchCtrl.searchBar;
}
searchBar 个性化设置
///将Cancel 修改为 取消
[[UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UISearchBar class]]] setTitle:@"取消"];
//修改取消文字颜色以及光标的颜色
self.searchCtrl.searchBar.tintColor = [UIColor redColor];
//textField 设置圆角
UITextField *textField = (UITextField *)[self.searchCtrl.searchBar valueForKey:@"_searchField"];
textField.backgroundColor = [UIColor whiteColor];
textField.layer.borderColor = [UIColor redColor].CGColor;
textField.layer.borderWidth = 2.f;
textField.layer.cornerRadius = 14.f;
textField.placeholder = @"大家好";
textField.tintColor = [UIColor blueColor];
textField.clipsToBounds = YES;
//去除灰色背景
if (@available(iOS 11, *)) {
for (UIView *view in textField.subviews) {
if ([view isKindOfClass:NSClassFromString(@"_UISearchBarSearchFieldBackgroundView")]) {
[view removeFromSuperview];
}
}
}
//取消上下两条线
for (UIView *view in self.searchCtrl.searchBar.subviews.firstObject.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
[view removeFromSuperview];
}
}
//调节放大镜的位置
[self.searchCtrl.searchBar setPositionAdjustment:UIOffsetMake(100.f, 0.f) forSearchBarIcon:UISearchBarIconSearch];
京东搜索框
下面我们通过模仿京东首页的搜索框来强化一下 UISearchController
的结构 具体效果如下图:
通过上图我们可以看到这个搜索框自带一些动画效果,而系统并没有为我们提供这些动画效果,为了更方便的实现上述效果 这里我采取了自定义 SearchController的方式 这样我们实现起来也就更加灵活 也不用去适配 SearchController在不同版本下的差异了.
首先我们自定义一个继承自 UIViewController
的控制器,在该控制器中 定义两个代理方法(其实就是从UISearchController
中拿过来的,稍微改了下前缀) 如下:
@class GSJDSearchVC;
@protocol GSSearchControllerDelegate <NSObject>
@optional
- (void)didPresentSearchController:(GSJDSearchVC *)searchController;
- (void)didDismissSearchController:(GSJDSearchVC *)searchController;
@end
@protocol GSSearchResultsUpdating <NSObject>
@required
// Called when the search bar's text or scope has changed or when the search bar becomes first responder.
- (void)updateSearchResultsForSearchController:(GSJDSearchVC *)searchController;
@end
@interface GSJDSearchVC : UIViewController
@property (nullable, nonatomic, weak) id <GSSearchResultsUpdating> searchResultsUpdater;
@property (nullable, nonatomic, weak) id <GSSearchControllerDelegate> delegate;
@property (nonatomic, strong, readonly) GSJDSearchBar *searchBar;
@property (nullable, nonatomic, strong, readonly) UIViewController *searchResultsController;
- (instancetype)initWithSearchResultsController:(nullable UIViewController *)searchResultsController;
@end
然后在initWithSearchResultsController:
方法中将结果控制器添加进来
-(instancetype)initWithSearchResultsController:(UIViewController *)searchResultsController{
self = [super init];
if (searchResultsController) {
_searchResultsController = searchResultsController;
CGFloat yCoordinate = 64.f;
if (GS_iPhoneX) {
yCoordinate = 88.f;
}
self.view.frame = CGRectMake(0.f, yCoordinate, SCREENSIZE.width, SCREENSIZE.height);
[self addChildViewController:_searchResultsController];
_searchResultsController.view.frame = self.view.bounds;
[self.view addSubview:_searchResultsController.view];
}
return self;
}
最后根据 SearchBar
的内容来决定是否将 GSJDSearchVC
添加到控制器中
- (void)textChange{
if (self.searchBar.text.length == 1 && self.delegate) {
[self.delegate didPresentSearchController:self];
}
if (self.searchBar.text.length == 0 && self.delegate) {
[self.delegate didDismissSearchController:self];
}
if (self.searchResultsUpdater) {
[self.searchResultsUpdater updateSearchResultsForSearchController:self];
}
}
这里我只摘了部分代码 感兴趣的同学可以去 GitHub 看看具体的代码实现