序言
最近項目中要用到類似百度外賣app篩選器,于是自己動手去實現(xiàn)吝羞。本文將詳盡的利用DataSource和Delegate設(shè)計模式去實現(xiàn)(OC)尿背。但由于筆者知識水平有限逛万,沒有考慮內(nèi)存優(yōu)化的問題弟疆,一些代碼規(guī)范和設(shè)計思維存在錯誤戚长,懇請前輩們批評指正。
效果圖
設(shè)計思路
在布局方面采用的是Masonry怠苔,也有用到YYKit的一些方法同廉,一些宏編譯的色值可替換,這里就不給出具體的色值柑司。先來看看FilterViewDelegate
和FilterViewDataSource
@protocol FilterViewDelegate <NSObject>
@optional
- (void)filterView:(FilterView *)filterView willDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index;
- (void)filterView:(FilterView *)filterView didDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index;
@required
- (void)filterView:(FilterView *)filterView didSelectIndex:(NSInteger)index inItemIndx:(NSInteger)itemIndex;
@end
其中方法:
- (void)filterView:(FilterView *)filterView willDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index;
- (void)filterView:(FilterView *)filterView didDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index;
分別為將要顯示篩選菜單和菜單顯示后的代理迫肖,可不必具體實現(xiàn)。
- (void)filterView:(FilterView *)filterView didSelectIndex:(NSInteger)index inItemIndx:(NSInteger)itemIndex;
這個方法表示從哪一個Item中選擇了具體的哪一行攒驰,這個方法必須具體實現(xiàn)咒程。
@protocol FilterViewDataSource <NSObject>
@required
- (NSInteger)numberOfItemInFilterView:(FilterView *)filterView;
- (NSString *)filterView:(FilterView *)filterView titleForItem:(NSInteger)item;
- (NSInteger)filterView:(FilterView *)filterView numberOfRowsForItem:(NSInteger)item;
- (NSString *)filterView:(FilterView *)filterView titleForRows:(NSInteger)row item:(NSInteger)item;
@end
其中FilterViewDataSource中的每個辦法必須實現(xiàn),下面是幾個方法的介紹:
- (NSInteger)numberOfItemInFilterView:(FilterView *)filterView;
該方法表示有多少個Item讼育,即有多少個篩選項,如效果圖就有4個帥選項。
- (NSString *)filterView:(FilterView *)filterView titleForItem:(NSInteger)item;
每個篩選項的標(biāo)題
- (NSInteger)filterView:(FilterView *)filterView numberOfRowsForItem:(NSInteger)item;
每個篩選項下的篩選菜單的行數(shù)
- (NSString *)filterView:(FilterView *)filterView titleForRows:(NSInteger)row item:(NSInteger)item;
每個篩選項下的篩選菜單的標(biāo)題
具體實現(xiàn)
在FilterView.h
定義如下:
@interface FilterView : UIView
@property(nonatomic,weak) id<FilterViewDelegate> delegate;
@property(nonatomic,weak) id<FilterViewDataSource> dataSource;
- (void)hideMenu;
@end
然后在FilterView.m
中定義一些數(shù)據(jù)如下:
@interface FilterView () <UITableViewDelegate,UITableViewDataSource>
//標(biāo)題數(shù)組
@property (nonatomic,strong) NSMutableArray <NSString*> *itemTitleArray;
//標(biāo)題下帥選項數(shù)組奶段,二維數(shù)組
@property (nonatomic,strong) NSMutableArray *dataArray;
//帥選列表
@property (nonatomic,weak) UITableView *tableView;
@end
@implementation FilterView {
//菜單數(shù)目
NSInteger itemCount;
UIWindow *window;
//當(dāng)前選中的ITEM下標(biāo)
NSInteger currentSelectItemIndex;
//當(dāng)前選中的菜單項
UIView *currentSeleectItemView;
//是否正在顯示帥選菜單
BOOL isShowMenu;
//遮罩層
UIView *maskView;
}
重寫UIView
的初始化方法饥瓷,主要是初始化定義的一些數(shù)據(jù):
- (instancetype)init {
self = [super init];
if (self) {
window = [[UIApplication sharedApplication] keyWindow];
self.itemTitleArray = [[NSMutableArray alloc] initWithCapacity:0];
self.dataArray = [[NSMutableArray alloc] initWithCapacity:0];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
window = [[UIApplication sharedApplication] keyWindow];
self.itemTitleArray = [[NSMutableArray alloc] initWithCapacity:0];
self.dataArray = [[NSMutableArray alloc] initWithCapacity:0];
}
return self;
}
然后通過setter
的方式將具體實現(xiàn)的數(shù)據(jù)源保存起來,具體代碼如下:
- (void)setDataSource:(id<FilterViewDataSource>)dataSource {
_dataSource = dataSource;
if ([_dataSource respondsToSelector:@selector(numberOfItemInFilterView:)]) {
//有多少個item
itemCount = [_dataSource numberOfItemInFilterView:self];
}
if ([_dataSource respondsToSelector:@selector(filterView:titleForItem:)]) {
for (int i = 0; i < itemCount; i ++) {
//item的標(biāo)題
NSString *title = [_dataSource filterView:self titleForItem:i];
NSLog(@"title:%@",title);
[_itemTitleArray addObject:title];
}
}
if ([_dataSource respondsToSelector:@selector(filterView:numberOfRowsForItem:)]) {
for (int i = 0; i < itemCount; i ++) {
//每個item下的帥選行數(shù)
NSInteger rows = [_dataSource filterView:self numberOfRowsForItem:i];
NSMutableArray *titleArray = [[NSMutableArray alloc] initWithCapacity:rows];
//先用""默認(rèn)填空
for (int row = 0; row < rows; row ++) {
[titleArray addObject:@""];
}
[_dataArray addObject:titleArray];
}
}
if ([_dataSource respondsToSelector:@selector(filterView:titleForRows:item:)]) {
for (int i = 0; i < itemCount; i ++) {
NSMutableArray *titleArray = [_dataArray[i] mutableCopy];
for (int k = 0; k < titleArray.count; k ++) {
//替換之前填空的數(shù)據(jù)
_dataArray[i][k] = [_dataSource filterView:self titleForRows:k item:i];
}
}
}
//保存數(shù)據(jù)再初始化視圖
[self setupView];
}
初始化視圖前痹籍,用- (UIView *)createItemView:(NSString *)title
方法呢铆,通過傳入菜單標(biāo)題,即可創(chuàng)建itemView蹲缠,即一個菜單項棺克。方法具體實現(xiàn)為:
- (UIView *)createItemView:(NSString *)title {
UIView *itemView = [[UIView alloc] init];
itemView.backgroundColor = [UIColor whiteColor];
UILabel *itemTitleLabel = [[UILabel alloc] init];
[itemView addSubview:itemTitleLabel];
itemTitleLabel.font = [UIFont systemFontOfSize:11];
itemTitleLabel.textColor = MainThemeColor;
itemTitleLabel.text = title;
[itemTitleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.height.equalTo(itemView);
make.centerX.equalTo(itemView).offset(-8);
make.left.greaterThanOrEqualTo(itemView);
}];
UIButton *arrowButton = [UIButton buttonWithType:UIButtonTypeCustom];
[itemView addSubview:arrowButton];
//為了防止點到箭頭按鈕
arrowButton.userInteractionEnabled = NO;
UIImage *normalImage = [UIImage imageNamed:@"arrow_normal"];
UIImage *selectedImage = [UIImage imageNamed:@"arrow_selected"];
[arrowButton setBackgroundImage:normalImage forState:UIControlStateNormal];
[arrowButton setBackgroundImage:selectedImage forState:UIControlStateSelected];
//給箭頭按鈕設(shè)置一個tag
arrowButton.tag = ArrowButtonTag;
[arrowButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(itemView);
make.left.equalTo(itemTitleLabel.mas_right).offset(0);
make.right.lessThanOrEqualTo(itemView);
make.size.mas_equalTo(normalImage.size);
}];
//設(shè)置菜單欄的tag
itemView.tag = [_itemTitleArray indexOfObject:title];
//設(shè)置圓角屬性
itemView.layer.masksToBounds = YES;
itemView.layer.cornerRadius = 8.0f;
return itemView;
}
好了,準(zhǔn)備工作就緒线定,現(xiàn)在開始初始化視圖創(chuàng)建菜單欄:
//布局參照物
UIView *leftView = self;
for (int i = 0; i < itemCount ; i ++) {
UIView *itemView = [self createItemView:_itemTitleArray[i]];
[self addSubview:itemView];
[itemView mas_makeConstraints:^(MASConstraintMaker *make) {
if (i) {
make.width.equalTo(leftView);
make.left.equalTo(leftView.mas_right).offset(8);
}else {
make.left.equalTo(leftView).offset(2);
}
make.top.height.bottom.equalTo(self);
make.height.equalTo(@30);
if (i == itemCount - 1) {
make.right.equalTo(self).offset(-2);
}
}];
leftView = itemView;
}
因為在顯示篩選菜單的時候需要一個遮罩層娜谊,所以加上遮罩層初始化的代碼:
//初始化遮罩層
maskView = [[UIView alloc] initWithFrame:window.bounds];
//先隱藏,顯示篩選菜單的時候再顯示
maskView.hidden = YES;
[window addSubview:maskView];
初始化顯示篩選數(shù)據(jù)用的tableView斤讥,具體代碼如下:
//初始化篩選菜單的tableView
UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView = tableView;
_tableView.dataSource = self;
_tableView.delegate = self;
_tableView.tableFooterView = [UIView new];
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:cellID];
[window addSubview:_tableView];
//先設(shè)置左右約束的布局纱皆,顯示的時候再跟新頂部約束調(diào)整布局
[_tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(window);
}];
實現(xiàn)UITableView
的UITableViewDataSource
#pragma mark - UITableViewDelegate,UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSArray *rows = _dataArray[currentSelectItemIndex];
return rows.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 30;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
cell.textLabel.text = _dataArray[currentSelectItemIndex][indexPath.row];
cell.textLabel.font = [UIFont systemFontOfSize:12];
return cell;
}
至此,視圖的初始化工作完成芭商。下面來實現(xiàn)顯示和消失的動畫:
****顯示動畫****
- (void)showMenuForm:(UIView *)itemView {
//記錄當(dāng)前顯示的菜單欄下標(biāo)
currentSelectItemIndex = itemView.tag;
//刷新數(shù)據(jù)
[_tableView reloadData];
//更新tableview的頂部約束
[_tableView mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(window).offset(self.frame.origin.y + 30 + 64 + 1);
make.height.equalTo(@(_tableView.contentSize.height));
}];
//顯示遮罩層派草,同時加上手勢點擊事件
maskView.hidden = NO;
UITapGestureRecognizer *tapMaskView = [[UITapGestureRecognizer alloc] initWithActionBlock:^(id _Nonnull sender)
{
//消失動畫
[self hideMenu];
}];
maskView.userInteractionEnabled = YES;
maskView.backgroundColor = MainDimBackgroundColor;
[maskView addGestureRecognizer:tapMaskView];
[maskView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_tableView.mas_bottom);
make.left.right.bottom.equalTo(window);
}];
//具體的動畫實現(xiàn)
CGRect frame = _tableView.frame;
_tableView.frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, 0);
[UIView animateWithDuration:0.2 animations:^{
_tableView.frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, _tableView.contentSize.height);
maskView.alpha = 1.0;
} completion:^(BOOL finished) {
}
}];
}
這里需要注意的是,在自身父視圖中完成布局后铛楣,才得到self.frame.origin.y
近迁,然后才在window
得到_tableView
的頂部偏移值,就暫時這樣解決簸州。如果有更好的方法鉴竭,歡迎一起探討。
添加手勢時勿侯,選擇了YYKit中對手勢擴展的方法拓瞪,通過block
的形式,方便直觀理解助琐。
****消失動畫****
- (void)hideMenu {
//改變菜單箭頭狀態(tài)
UIButton *arrowButton = [currentSeleectItemView viewWithTag:ArrowButtonTag];
arrowButton.selected = NO;
CGRect frame = _tableView.frame;
[UIView animateWithDuration:0.2 animations:^{
_tableView.frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, 0);
maskView.alpha = 0.f;
} completion:^(BOOL finished) {
}];
}
因為這里消失動畫有很多個入口祭埂,所以消失前獲取菜單欄的按鈕,設(shè)置選定值為NO
動畫設(shè)計好之后兵钮,最重要的一步蛆橡,就是給每一個菜單欄添加點擊手勢,從而實現(xiàn)箭頭的變化和菜單選項的顯示和消失動畫掘譬,下面是實現(xiàn)代碼:
itemView.userInteractionEnabled = YES;
UITapGestureRecognizer *tapItemView = [[UITapGestureRecognizer alloc] initWithActionBlock:^(id _Nonnull sender) {
UIView *tempItemView = ((UITapGestureRecognizer *)sender).view;
UIButton *arrowButton = [tempItemView viewWithTag:ArrowButtonTag];
arrowButton.selected = !arrowButton.selected;
if (arrowButton.selected) {
if (isShowMenu) {
//改變上一菜單的箭頭狀態(tài)
UIButton *lastArrowButton = [currentSeleectItemView viewWithTag:ArrowButtonTag];
lastArrowButton.selected = NO;
[self hideMenu];
}
if (isShowMenu) {
NSLog(@"菜單正在顯示,先隱藏后再顯示");
}else {
NSLog(@"菜單沒在顯示,直接顯示");
}
//這里需要延遲執(zhí)行泰演,不然會出現(xiàn)隱藏后不顯示的問題
[self performSelector:@selector(showMenuForm:) withObject:tempItemView afterDelay:0.1];
isShowMenu = YES;
}else {
NSLog(@"菜單正在顯示,直接隱藏");
[self hideMenu];
isShowMenu = NO;
}
currentSeleectItemView = tempItemView;
}];
[itemView addGestureRecognizer:tapItemView];
上面的代碼可在創(chuàng)建itemView
之后添加。需要特別注意的是葱轩,當(dāng)前菜單選項正在顯示的時候睦焕,點擊另外一個菜單欄繼續(xù)顯示的時候藐握,我是先調(diào)用消失的動畫再顯示,若不添加延遲顯示的方法垃喊,會顯示不出來猾普。
最后實現(xiàn)FilterViewDelegate
,在動畫顯示前添加下列代碼:
if ([self.delegate respondsToSelector:@selector(filterView:willDisplayMenuFromItemView:forIndex:)]) {
[self.delegate filterView:self willDisplayMenuFromItemView:itemView forIndex:currentSelectItemIndex];
}
顯示動畫執(zhí)行完畢后:
if (finished) {
if ([self.delegate respondsToSelector:@selector(filterView:didDisplayMenuFromItemView:forIndex:)]) {
[self.delegate filterView:self willDisplayMenuFromItemView:itemView forIndex:currentSelectItemIndex];
}
}
在UITableViewDelegate
的- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
實現(xiàn)我們的代理方法:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if ([self.delegate respondsToSelector:@selector(filterView:didSelectIndex:inItemIndx:)]) {
[self.delegate filterView:self didSelectIndex:indexPath.row inItemIndx:currentSelectItemIndex];
}
}
具體用法示列
在某個ViewController
中繼承FilterViewDelegate
和FilterViewDataSource
itemArray = @[@"酒款品種",@"出產(chǎn)年份",@"陳釀年份",@"品飲分?jǐn)?shù)"];
dataArray = @[@[@"酒款品種1",@"酒款品種2",@"酒款品種3",@"酒款品種4"],@[@"出產(chǎn)年份1",@"出產(chǎn)年份2",@"出產(chǎn)年份3"],@[@"釀年份1",@"釀年份2"],@[@"品飲分?jǐn)?shù)1",@"品飲分?jǐn)?shù)2",@"品飲分?jǐn)?shù)3",@"品飲分?jǐn)?shù)4",@"品飲分?jǐn)?shù)5"]];
FilterView *filterView = [[FilterView alloc] initWithFrame:CGRectZero];
filterView.dataSource = self;
filterView.delegate = self;
[self.view addSubview:filterView];
[filterView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.width.equalTo(self.view);
make.top.equalTo(self.view).offset(2);
}];
#pragma mark - FilterViewDelegate,FilterViewDataSource
- (NSInteger)numberOfItemInFilterView:(FilterView *)filterView {
return itemArray.count;
}
- (NSString *)filterView:(FilterView *)tableView titleForItem:(NSInteger)item {
return itemArray[item];
}
- (NSInteger)filterView:(FilterView *)filterView numberOfRowsForItem:(NSInteger)item {
return ((NSArray *)dataArray[item]).count;
}
- (NSString *)filterView:(FilterView *)filterView titleForRows:(NSInteger)row item:(NSInteger)item {
return dataArray[item][row];
}
- (void)filterView:(FilterView *)filterView willDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index
{
NSLog(@"willDisplayMenu");
}
- (void)filterView:(FilterView *)filterView didDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index
{
NSLog(@"didDisplayMenu");
}
- (void)filterView:(FilterView *)filterView didSelectIndex:(NSInteger)index inItemIndx:(NSInteger)itemIndex {
NSLog(@"didSelectIndex");
[filterView hideMenu];
NSLog(@"reslut--%@",[NSString stringWithFormat:@"篩選結(jié)果:%@,%@",itemArray[itemIndex],dataArray[itemIndex][index]]);
}
結(jié)語
由于初次寫文章本谜,表達(dá)能力有限初家,不到之處敬請諒解。筆者是準(zhǔn)大四的學(xué)生乌助,歡迎志同道合人士一同探討溜在,深入學(xué)習(xí)。文章出現(xiàn)紕漏之處在所難免他托,懇請前輩們批評指正掖肋,不勝感激。