使用DataSource和Delegate设计模式实现一个筛选器

序言

最近项目中要用到类似百度外卖app筛选器,于是自己动手去实现。本文将详尽的利用DataSource和Delegate设计模式去实现(OC)。但由于笔者知识水平有限,没有考虑内存优化的问题,一些代码规范和设计思维存在错误,恳请前辈们批评指正。

效果图


使用DataSource和Delegate设计模式实现一个筛选器_第1张图片

设计思路


在布局方面采用的是Masonry,也有用到YYKit的一些方法,一些宏编译的色值可替换,这里就不给出具体的色值。先来看看FilterViewDelegateFilterViewDataSource

@protocol FilterViewDelegate 

@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;
分别为将要显示筛选菜单和菜单显示后的代理,可不必具体实现。

- (void)filterView:(FilterView *)filterView didSelectIndex:(NSInteger)index inItemIndx:(NSInteger)itemIndex;
这个方法表示从哪一个Item中选择了具体的哪一行,这个方法必须具体实现。

@protocol FilterViewDataSource 

@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中的每个办法必须实现,下面是几个方法的介绍:

- (NSInteger)numberOfItemInFilterView:(FilterView *)filterView;
该方法表示有多少个Item,即有多少个筛选项,如效果图就有4个帅选项。

- (NSString *)filterView:(FilterView *)filterView titleForItem:(NSInteger)item;
每个筛选项的标题

- (NSInteger)filterView:(FilterView *)filterView numberOfRowsForItem:(NSInteger)item;
每个筛选项下的筛选菜单的行数

- (NSString *)filterView:(FilterView *)filterView titleForRows:(NSInteger)row item:(NSInteger)item;
每个筛选项下的筛选菜单的标题

具体实现

FilterView.h定义如下:

@interface FilterView : UIView

@property(nonatomic,weak) id delegate;
@property(nonatomic,weak) id dataSource;

- (void)hideMenu;

@end

然后在FilterView.m中定义一些数据如下:

@interface FilterView () 
//标题数组
@property (nonatomic,strong) NSMutableArray  *itemTitleArray;
//标题下帅选项数组,二维数组
@property (nonatomic,strong) NSMutableArray *dataArray;
//帅选列表
@property (nonatomic,weak) UITableView *tableView;
@end

@implementation FilterView {
    //菜单数目
    NSInteger itemCount;
    UIWindow *window;
    //当前选中的ITEM下标
    NSInteger currentSelectItemIndex;
    //当前选中的菜单项
    UIView *currentSeleectItemView;
    //是否正在显示帅选菜单
    BOOL isShowMenu;
    //遮罩层
    UIView *maskView;
}

重写UIView的初始化方法,主要是初始化定义的一些数据:

- (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的方式将具体实现的数据源保存起来,具体代码如下:

- (void)setDataSource:(id)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的标题
            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下的帅选行数
            NSInteger rows = [_dataSource filterView:self numberOfRowsForItem:i];
            NSMutableArray *titleArray = [[NSMutableArray alloc] initWithCapacity:rows];
            //先用""默认填空
            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 ++) {
                //替换之前填空的数据
                _dataArray[i][k] = [_dataSource filterView:self titleForRows:k item:i];
            }
        }
    }
     //保存数据再初始化视图
    [self setupView];
}

初始化视图前,用- (UIView *)createItemView:(NSString *)title方法,通过传入菜单标题,即可创建itemView,即一个菜单项。方法具体实现为:

- (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];
    //给箭头按钮设置一个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);
    }];
    
    //设置菜单栏的tag
    itemView.tag = [_itemTitleArray indexOfObject:title];
    //设置圆角属性
    itemView.layer.masksToBounds = YES;
    itemView.layer.cornerRadius = 8.0f;
    return itemView;
}

好了,准备工作就绪,现在开始初始化视图创建菜单栏:

    //布局参照物
    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];

初始化显示筛选数据用的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];
    //先设置左右约束的布局,显示的时候再跟新顶部约束调整布局
    [_tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.right.equalTo(window);
    }];

实现UITableViewUITableViewDataSource

#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;
}

至此,视图的初始化工作完成。下面来实现显示和消失的动画:

****显示动画****

- (void)showMenuForm:(UIView *)itemView {
    //记录当前显示的菜单栏下标
    currentSelectItemIndex = itemView.tag;
    //刷新数据
    [_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);
    }];
  
    //具体的动画实现
    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 {
    //改变菜单箭头状态
    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) {

    }];
}

因为这里消失动画有很多个入口,所以消失前获取菜单栏的按钮,设置选定值为NO

动画设计好之后,最重要的一步,就是给每一个菜单栏添加点击手势,从而实现箭头的变化和菜单选项的显示和消失动画,下面是实现代码:

 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) {
                //改变上一菜单的箭头状态
                UIButton *lastArrowButton = [currentSeleectItemView viewWithTag:ArrowButtonTag];
                lastArrowButton.selected = NO;
                [self hideMenu];
            }
            if (isShowMenu) {
                NSLog(@"菜单正在显示,先隐藏后再显示");
            }else {
                NSLog(@"菜单没在显示,直接显示");
            }

            //这里需要延迟执行,不然会出现隐藏后不显示的问题
            [self performSelector:@selector(showMenuForm:) withObject:tempItemView afterDelay:0.1];
            isShowMenu = YES;
        }else {
            NSLog(@"菜单正在显示,直接隐藏");
            [self hideMenu];
            isShowMenu = NO;
        }
        currentSeleectItemView = tempItemView;

    }];
    
    [itemView addGestureRecognizer:tapItemView];

上面的代码可在创建itemView之后添加。需要特别注意的是,当前菜单选项正在显示的时候,点击另外一个菜单栏继续显示的时候,我是先调用消失的动画再显示,若不添加延迟显示的方法,会显示不出来。


最后实现FilterViewDelegate,在动画显示前添加下列代码:

 if ([self.delegate respondsToSelector:@selector(filterView:willDisplayMenuFromItemView:forIndex:)]) {
        [self.delegate filterView:self willDisplayMenuFromItemView:itemView forIndex:currentSelectItemIndex];
    }

显示动画执行完毕后:

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实现我们的代理方法:

- (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中继承FilterViewDelegateFilterViewDataSource

    itemArray = @[@"酒款品种",@"出产年份",@"陈酿年份",@"品饮分数"];
    dataArray = @[@[@"酒款品种1",@"酒款品种2",@"酒款品种3",@"酒款品种4"],@[@"出产年份1",@"出产年份2",@"出产年份3"],@[@"酿年份1",@"酿年份2"],@[@"品饮分数1",@"品饮分数2",@"品饮分数3",@"品饮分数4",@"品饮分数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:@"筛选结果:%@,%@",itemArray[itemIndex],dataArray[itemIndex][index]]);

}

结语

由于初次写文章,表达能力有限,不到之处敬请谅解。笔者是准大四的学生,欢迎志同道合人士一同探讨,深入学习。文章出现纰漏之处在所难免,恳请前辈们批评指正,不胜感激。

你可能感兴趣的:(使用DataSource和Delegate设计模式实现一个筛选器)