自定义UISearchBar和UISearchDisplayController
起因
近期由于公司项目升级,UI变化较大,之前项目中的搜索一直是使用的系统自带组件UISearchBar和UISearchDisplayController,然而根据UI设计图,该系统组件无法满足需求,故需要自定义该组件以实现类似微信的搜索功能,在百度无果的情况下,终于决定自己动手完全从头自定义,特写此文以提供思路给有同样需求的同行。
步骤
- 定义SearchTextField相关组件和代理
- 定义SearchBar相关组件和代理
- 定义SearchDisplayController相关组件和代理
效果图
按照惯例先上一张效果图,如下所示,由于重点在于搜索控件,所以tableView中的数据并非真实数据。
主要实现代码
实现SearchTextField组件及代理
由于我需要的效果是搜索框图片位于左边和中央的位置,而且还需要一层边框,所以CustomSearchTextField主要组成有:CustomSearchTextFieldBackgroundView(背景层),UISearchTextField(输入框),UIImageView(搜索提示按钮),UILabel(占位文本标签)
@interface CustomSearchTextField ()
@property(nonatomic, strong) UIButton* _searchTextClearButton;
@property(nonatomic, strong) UIView* _searchTextFieldBackgroudView;
@property(nonatomic, strong) UILabel* _searchTextFieldPlaceHolderLabel;
@property(nonatomic, strong) UIImageView* _searchTextFieldIcon;
@property(nonatomic, strong) UITextField* _searchTextField;
@end
省略了各个UI控件的初始化代码,主要的布局代码在layoutSubviews中,代码如下所示:
[UIView animateWithDuration:0.25f animations:^{
[__searchTextFieldBackgroudView setFrame:self.bounds];
if(__searchTextField.isFirstResponder) {
[__searchTextFieldIcon setFrame:CGRectMake(10, __searchTextFieldIcon.frame.origin.y, __searchTextFieldIcon.frame.size.width, __searchTextFieldIcon.frame.size.height)];
[__searchTextField setFrame:CGRectMake(__searchTextFieldIcon.frame.origin.x + __searchTextFieldIcon.frame.size.width + 5, __searchTextField.frame.origin.y, __searchTextFieldBackgroudView.bounds.size.width - __searchTextFieldIcon.frame.origin.x - __searchTextFieldIcon.frame.size.width - 15, __searchTextField.frame.size.height)];
[__searchTextFieldPlaceHolderLabel setFrame:__searchTextField.bounds];
}else {
[__searchTextFieldIcon setCenter:CGPointMake(__searchTextFieldBackgroudView.bounds.size.width / 2 - 10, __searchTextFieldBackgroudView.bounds.size.height / 2)];
[__searchTextField setFrame:__searchTextFieldBackgroudView.bounds];
[__searchTextFieldPlaceHolderLabel setFrame:CGRectMake(__searchTextFieldBackgroudView.bounds.size.width / 2, __searchTextField.frame.origin.y, __searchTextFieldBackgroudView.bounds.size.width / 2 - 5, __searchTextField.bounds.size.height)];
}
}];
layoutSubviews代码说明
该段代码主要是做了两件事
如果该UITextField是firstResponder : 调整各个控件的frame,以动画形式使得搜索图标位于左侧,UITextField调整至合适大小,UILabel也调整到文本框左侧最开始处
如果该UITextField不是firstResponder : 调整各个控件的frame,以动画形式使得UITextField的frame充满整个背景层,搜索图标和UILabel调整至整个CustomSearchTextField的正中央
SearchTextField的代码到此就结束了,主要就是做了对UITextField的再封装,及对图标和占位文本的处理等等。
实现CustomSearchBar相关组件
参考UISearchBar头文件,该类主要实现了一个基本的SearchBar,包括对UITextField的基本状态的处理,留出外部调用的代理接口。
CustomSearchBar的主要组成
CustomSearchTextField主要组成是由一个背景层,一个搜索框,上下两条分隔线,取消按钮等组成,这些控件都放在分类里面,外部不可见,如下代码所示:
@interface CustomSearchBar () <CustomSearchTextFieldDelegate>
@property(nonatomic, strong) CMSearchTextField* searchTextField;
@property(nonatomic, strong) UIView* searchBarBackgroudView;
@property(nonatomic, strong) UIImageView* searchBarTopSeperatorLine;
@property(nonatomic, strong) UIImageView* searchBarBottomSeperatorLine;
@property(nonatomic, strong) UIButton* cancelSearchButton;
@property(nonatomic, assign) BOOL shouldShowCancelButton;
@end
而由于本人水平所限,不知道系统的UISearchBar是如何做到看上去似乎与UISearchDisplayController毫无关联,故我在CustomSearchBar中保留了一个CustomSearchDisplayController属性,使得在输入框状态变化时能调用CustomSearchDisplayController中的相关代理方法,故CustomSearchBar类如下代码所示:
@interface CustomSearchBar : UIView
@property(nonatomic, copy) NSString* placeholder;
@property(nonatomic, copy) NSString* text;
@property(nonatomic, weak) CMSearchDisplayController* searchDisplayController;
@property(nonatomic, assign) id<CMSearchBarDelegate> delegate;
@end
其中最主要的frame调整依然是在layoutSubViews方法中,相关调整代码如下:
-(void)layoutSubviews {
[UIView animateWithDuration:0.25f animations:^{
[_searchBarBackgroudView setFrame:self.bounds];
[_searchBarTopSeperatorLine setFrame:CGRectMake(0, 0, _searchBarBackgroudView.bounds.size.width, kSingleLine)];
[_searchBarBottomSeperatorLine setFrame:CGRectMake(0, _searchBarBackgroudView.bounds.size.height - kSingleLine, _searchBarBackgroudView.bounds.size.width, kSingleLine)];
if(_shouldShowCancelButton) {
[_searchTextField setFrame:CGRectMake(20, 25, _searchBarBackgroudView.bounds.size.width - 60, _searchBarBackgroudView.bounds.size.height - 30)];
}else {
[_cancelSearchButton setHidden:YES];
[_searchTextField setFrame:CGRectMake(20, 5, _searchBarBackgroudView.bounds.size.width - 40, _searchBarBackgroudView.bounds.size.height - 10)];
}
} completion:^(BOOL finished) {
if(finished) {
dispatch_async(dispatch_get_main_queue(), ^{
if(_shouldShowCancelButton) {
[_cancelSearchButton setHidden:NO];
[_cancelSearchButton setFrame:CGRectMake(_searchBarBackgroudView.bounds.size.width - 5 - _cancelSearchButton.bounds.size.width, _cancelSearchButton.frame.origin.y, _cancelSearchButton.bounds.size.width, _cancelSearchButton.bounds.size.height)];
[_cancelSearchButton setCenter:CGPointMake(_cancelSearchButton.center.x, _searchTextField.center.y)];
}
});
}
}];
return [super layoutSubviews];
}
layoutSubviews代码说明
变量__shouldShowCancelButton,是用于指示是否显示取消按钮的BOOL变量,该按钮只有在激活状态下才会显示,故此变量可用于指示搜索框是否处于激活状态下(所谓激活状态就是指UITextField是否成为firstResponder),如果是处于激活状态下,调整整个CustomSearchBackgroundView子控件的frame下移20个单位,即状态栏的高度,如果处于未激活状态,则调整至CustomSearchBar的正中央。
实现CustomSearchDisplayController相关组件
CustomSearchDisplayController类组成
参考UISearchDisplayController类中的方法,CustomSearchDisplayController类情况如下所示:
@interface CMSearchDisplayController : NSObject
-(instancetype)initWithSearchBar:(CMSearchBar*)searchBar contentsController:(UIViewController*)viewController;
@property(nonatomic, strong, readonly) CMSearchBar* searchBar;
@property(nonatomic, strong, readonly) UIViewController* searchContentsController;
@property(nonatomic, strong, readonly) UITableView* searchResultsTableView;
@property(nonatomic, assign) id<UITableViewDataSource> searchResultsDataSource;
@property(nonatomic, assign) id<UITableViewDelegate> searchResultsDelegate;
@property(nonatomic, assign) id<CMSearchDisplayControllerDelegate> delegate;
@property (nonatomic, assign) BOOL isActive;
-(void)setActive:(BOOL)bActive;
-(void)setActive:(BOOL)bActive animated:(BOOL)animated;
@end
其中主要包括一个searchBar,一个tableView,一个contentsController实例,还有用于tableview的dataSource和delegate的两个代理及用于外部实现的SearchDisplayControllerDelegate,其中最主要的代码便要数setActive:animated中的处理,如下所示:
-(void)setActive:(BOOL)bActive animated:(BOOL)animated {
_isActive = bActive;
CGFloat animateDuring = animated ? 0.25f : 0.0f;
if(_searchContentsController.navigationController == nil)
return ;
if(bActive) {
[_searchContentsController.navigationController.navigationBar setFrame:CGRectMake(0, -44, _searchContentsController.navigationController.navigationBar.bounds.size.width, _searchContentsController.navigationController.navigationBar.bounds.size.height)];
UIImageView* dimmView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 44, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
[dimmView setUserInteractionEnabled:YES];
[dimmView setImage:[[self createImageWithColor:[UIColor colorWithRed:247.0f / 255.0f green:247.0f / 255.0f blue:247.0f / 255.0f alpha:0.9f] size:dimmView.bounds.size] applyLightEffect]];
[[UIApplication sharedApplication].keyWindow addSubview:dimmView];
_searchBarPreviousSuperView = _searchBar.superview;
_searchBarPreviousSuperRect = _searchBar.frame;
[_searchBar removeFromSuperview];
[dimmView addSubview:_searchBar];
[_searchBar setFrame:CGRectMake(0, 0, _searchBar.bounds.size.width, _searchBar.bounds.size.height + 20)];
[UIView animateWithDuration:animateDuring animations:^{
[_searchContentsController.view setFrame:CGRectMake(0, -44, _searchContentsController.view.bounds.size.width, _searchContentsController.view.bounds.size.height + 44)];
[dimmView setFrame:CGRectMake(0, 0, dimmView.bounds.size.width, dimmView.bounds.size.height)];
} completion:^(BOOL finished) {
if(finished) {
dispatch_async(dispatch_get_main_queue(), ^{
[_searchResultsTableView setFrame:CGRectMake(0, _searchBar.bounds.size.height, dimmView.bounds.size.width, dimmView.bounds.size.height - _searchBar.bounds.size.height)];
[dimmView addSubview:_searchResultsTableView];
if([self.delegate respondsToSelector:@selector(searchDisplayControllerDidBeginSearch:)])
[self.delegate searchDisplayControllerDidBeginSearch:self];
});
}
}];
}else {
[_searchResultsTableView removeFromSuperview];
[UIView animateWithDuration:0.25f animations:^{
[_searchContentsController.navigationController.navigationBar setFrame:CGRectMake(0, 20, _searchContentsController.navigationController.navigationBar.bounds.size.width, _searchContentsController.navigationController.navigationBar.bounds.size.height)];
[_searchBar.superview setFrame:CGRectMake(0, 64, _searchBar.superview.bounds.size.width, _searchBar.superview.bounds.size.height)];
[_searchContentsController.view setFrame:CGRectMake(0, 0, _searchContentsController.view.bounds.size.width, _searchContentsController.view.bounds.size.height - 44)];
[_searchBar setFrame:CGRectMake(0, 0, _searchBar.bounds.size.width, _searchBar.bounds.size.height - 20)];
}completion:^(BOOL finished) {
if(finished) {
dispatch_async(dispatch_get_main_queue(), ^{
[_searchBar.superview removeFromSuperview];
[_searchBarPreviousSuperView addSubview:_searchBar];
if([self.delegate respondsToSelector:@selector(searchDisplayControllerDidEndSearch:)])
[self.delegate searchDisplayControllerDidEndSearch:self];
});
}
}];
}
return ;
}
setActive:animate:主要代码说明
contentsController指的是内容展示viewController,一般传递的是持有者本身,在此方法中主要做了以下几件事:
如果contentsController没有导航栏,则不需要调整任何控件
如果contentsController有导航栏,且处于激活状态
使用动画方式使得该UINavigationBar实例往上移动44像素个单位,然后创建遮罩层,为方便起见,我直接将他加在当前的keyWindow当中,记录CustomSearchBar的superView及在superView中的frame,再将它从父视图中移除,并加入到遮罩层dimmView当中,调整坐标为0,0,拉长20个高度以覆盖整个状态栏,显示取消按钮。如果contentsController有导航栏,且自于未激活状态
使用动画的方式使得该UINavigationBar实例重新移动到合适位置,然后遮罩层同步下移同样的高度,调整CustomSearchBar的高度,使之减少20个像素单位,回到未激活状态的位置,动画完成之后,删除遮罩层dimmView,CustomSearchBar从其superView(dimmView)中移出,并加入到原来它的superView当中,并设置位置为在原来superView当中的位置,隐藏取消按钮。
结语
由于本人水平所限,代码写的并不十分美观,也并不是十分高效,iOS大神们肯定会有更好的解决方案,本文旨在为那些需要此功能但还没有思路的同行们提供一点意见,iOS大神请无视,最后感谢所有阅读此文的人。