美文网首页
使用DataSource和Delegate设计模式实现一个筛选器

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

作者: Doramical | 来源:发表于2016-08-16 17:49 被阅读0次

    序言

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

    效果图


    设计思路


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

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

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

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

    - (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<FilterViewDelegate> delegate;
    @property(nonatomic,weak) id<FilterViewDataSource> dataSource;
    
    - (void)hideMenu;
    
    @end
    

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

    @interface FilterView () <UITableViewDelegate,UITableViewDataSource>
    //标题数组
    @property (nonatomic,strong) NSMutableArray <NSString*> *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<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的标题
                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设计模式实现一个筛选器

          本文链接:https://www.haomeiwen.com/subject/jgvpsttx.html