【iOS】3行代码轻松集成导航栏菜单--ZTNavigationMenu

背景

在做项目时需要用到筛选栏,本来想偷懒找个第三方,在gayhub下了几个,无外乎都是臃肿复杂,在我的想象中,这种库只需要简单的集成就好了,不需要关心太多东西。好吧,只能自己动手了 ╮(╯╰)╭

开始使用

获取控件


点击获取dome:ZTNavigationMenuDemo
目前还不支持pods集成,所以只能手动拉到项目里。
直接把ZTNavigationMenuDemo里的ZTNavigationMenu拉到项目里即可。

该控件使用到以下知识点


  • +load方法
  • runtime - 方法交换
  • UIView的视图层次与相应的方法
  • Masonry的使用
  • UIView动画

依赖


该控件使用的是自动布局,依赖Masonry库,如果你的项目没有,请集成进去。

简单集成


如果不需要自定义的话,只需要简单的3行代码即可集成该控件:

NSArray *data = [[NSArray alloc] initWithObjects:@"Objective-C", @"Swift", @"CPP", @"Java", @"PHP", @"SQL Server", nil]; // 数据源,并不需要一开始就给控件,你可以等到你需要的时候再给控件赋值
ZTNavigationMenuBlock block = ^(NSArray *data, int index) {
    NSLog(@"data = %@", [data objectAtIndex:index]);
}; // 点击cell的回调,同样的也不需要一开始就给控件

// 3行代码创建控件
ZTNavigationMenu *titleView = [[ZTNavigationMenu alloc] initWithBlock:block];
[titleView setData:data];
[[self navigationItem] setTitleView:titleView];

只需要这样子就OK了,显示效果如下:

未展开.png

展开.png

属性与方法


如果不满足默认效果,我还提供了一些属性与方法可以自定义控件。
属性说明:

@property(nonatomic, strong) NSArray *data;     // 数据源,目前只能是字符串数组;只需要赋值就会自动刷新,不像tableView调用reloadData方法才会刷新
@property(nonatomic, strong) UIFont *titleFont;             // 标题字体;如果该值非法则会调为默认
@property(nonatomic, strong) UIColor *titleColor;           // 标题颜色;如果该值非法则会调为默认值
@property(nonatomic, assign, readonly) int index;           // 选中cell的下标;当获取该值时,如果是-1,则表示没有选中的cell(可能data没数据)
@property(nonatomic, assign) int menuWidth;                 // 该控件的宽度,默认是140;取值范围是140~200;小于最小会被调整为最小值,大于最大会被调整为最大值
@property(nonatomic, assign) int cellHeight;                // cell的高度,默认50;取值范围是30~100;超出范围同样会被调整
@property(nonatomic, assign) int contentHeight;             // 显示内容的高度,默认200;取值范围是100~350;超出范围会被调整
@property(nonatomic, assign) int layoutGuideBottom;         // 遮罩距离底部的高度,默认0;取值范围1~150;当设置值为范围之外的时候,将会采取默认行为
@property(nonatomic, assign) UIBlurEffectStyle effectStyle; // 模糊风格,只有style为ZTBackgroundStyleEffect时才有效;默认UIBlurEffectStyleLight;非法值都被调整为默认值
@property(nonatomic, assign) CGFloat effectAlpha;           // 模糊透明度,默认0.8;取值范围0.1~1;超出范围会被调整
@property(nonatomic, assign) ZTBackgroundStyle style;       // 背景类型
@property(nonatomic, assign, getter=isTapBackgroundHidden) BOOL tapBackgroundHidden; // 点击遮罩可以隐藏Menu,YES是启用(默认YES)
@property(nonatomic, assign, getter=isRotate) BOOL rotate;                           // Menu的图片是否可以旋转,YES是可以旋转(默认YES)  

实例化方法:

// block可以不传,后续可以通过addBlock方法添加上去
+ (instancetype)navigationMenu;
+ (instancetype)navigationMenuWithBlock:(ZTNavigationMenuBlock)block;
- (instancetype)initWithBlock:(ZTNavigationMenuBlock)block;

方法说明:

- (void)addBlock:(ZTNavigationMenuBlock)block; // 更新回调的block
- (void)hiddenNavigationMenu; // 隐藏菜单
- (BOOL)updateImage:(NSString *)imageName andSize:(CGSize)size; // 更新Menu的图片和大小,如果图片获取失败则返回NO,并且使用默认图片和大小;size的范围是0~44,如果size的某个值非法的话,则会被调整到合法范围
- (BOOL)updateIndex:(int)index; // 更新index(切换选中的cell),如果index非法,则返回NO,如果Menu还没添加到导航栏,会返回YES但不代表一定能选中该index的cell,可能会因为data元素不够而无法选中
- (void)invalidate; // 销毁ZTNavigationMenu;不要调用removeFromSuperview和[[self navigationItem] setTitleView:nil]来销毁;一旦调用了该方法,则该控件就不可用了,需要再次使用的话,请重新创建一个

自定义显示效果


自定义显示效果我就只写一个,因为dome里都有,感兴趣的同学请下载dome自行查看。(其实是因为我懒。。。)
很多APP都是用模糊来代替半透明了,因为模糊实在是好看,所以我也实现了模糊遮罩,使用起来也很简单,只需要几句代码:

[titleView setStyle:ZTBackgroundStyleEffect];      // 开启模糊显示
[titleView setEffectStyle:UIBlurEffectStyleLight]; // 模糊类型
[titleView setEffectAlpha:0.8f];                   // 透明度
模糊遮罩.png

因为这些我用的都是默认值

[titleView setEffectStyle:UIBlurEffectStyleLight];
[titleView setEffectAlpha:0.8f];

所以事实上这些实现这句就可以了

[titleView setStyle:ZTBackgroundStyleEffect];

控件部分实现讲解

实例化


重写init方法,并且把默认值都在这里赋值;因为是继承于UIControl,所以可以直接在这里添加点击事件。

- (instancetype)init
{
    if (self = [super init])
    {
        _titleFont = kZTDefaultTitleFont;
        _titleColor = kZTDefaultTitleColor;
        _effectStyle = UIBlurEffectStyleLight;
        _style = ZTBackgroundStyleTranslucent;
        _tapBackgroundHidden = YES;
        _rotate = YES;
        _tempIndex = -1;
        _index = -1;
        _menuWidth = 140;
        _cellHeight = 50;
        _contentHeight = 200;
        _layoutGuideBottom = 0;
        _effectAlpha = 0.8f;
        
        [self setBackgroundColor:[UIColor clearColor]];
        [self addTarget:self action:@selector(tapCall) forControlEvents:UIControlEventTouchUpInside];
        [self createView];
    }

    return self;
}

初始化的时候主要是做了这些工作:设置默认值、绑定点击事件、创建添加到导航栏的view

创建contenView


当ZTNavigationMenu添加到导航栏的时候,会触发UIView的didMoveToSuperview方法,我们在该方法里创建contenView;因为didMoveToSuperview会多次触发,所以需要用个变量来记录第一次的触发,防止多次创建view。

- (void)didMoveToSuperview
{
    if ([self isAddToView]) 
    {
        return;
    }
    
    [self setAddToView:YES];
    [self setVC:[self getCurrentVC]];
    [self updateMenuWidth];
    [self createMenuView]; // 创建contenView
    [self setData:[self tempData]];
    [self updateIndex:[self tempIndex]];
    [self setTempData:nil];
    
    return;
}

方法重写的思路


因为contenView是在添加到导航栏之后才会创建的,所以在添加到导航栏之前对contenView进行配置的话会无效,因为contenView还不存在。当然,这不是我们想要的,所以我们要做些处理;
另外,当添加到导航栏之后进行配置,虽然contenView存在了,但它是按默认值去创建的,所以我们也要对其进行更新。
以下是我重写data方法的思路:

- (void)setData:(NSArray *)data
{
    if ([self isAddToView])
    {
        [self showData:data];
    }
    else
    {
        [self setTempData:data];
    }
    
    return;
}

- (void)showData:(NSArray *)data
{
    NSString *title = nil;
    NSUInteger count = [[self data] count];
    int i = 0; // 判断标志,0是刷新(默认),-1是删除;1不做操作
    
    // 判断data数量是不是0
    if ([data count] == 0)
    {
        if (count > 0) // 原来是有数据的,则删除数据
        {
            i = -1;
        }
        else
        {
            // 都没有数据,不做操作
            i = 1;
        }
        
        UIViewController *VC = [self VC];
        title = [VC title];
        _index = -1;
    }
    else
    {
        title = [data objectAtIndex:0];
        _index = 0;
    }
    
    [[self titleLabel] setText:title];
    _data = data;
    
    if (i == 1)
    {
        return;
    }
    
    // 刷新分组
    NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndex:0];
    kWeakSelf(weakSelf);
    [[self collectionView] performBatchUpdates:^{
        if (i == -1)
        {
            [[weakSelf collectionView] deleteSections:indexSet];
        }
        else
        {
            [[weakSelf collectionView] reloadSections:indexSet];
        }
    } completion:^(BOOL finished) {
    }];
    
    return;
}

如果contenView不存在,那么设置data会出问题,所以我们通过[self isAddToView]去判断是否已经添加到导航栏了,如果未添加,那么我们就把data保存到内部的tempData属性中,当触发didMoveToSuperview方法时,我们再通过调用showData方法显示数据。
如果已经添加到导航栏,那就直接调用showData刷新数据。

移除控件


当不需要的时候,我们就会想把它给干掉,但事实上该控件由多个部分组成的,并不能简单的通过removeFromSuperview去删除,这样子会删除不干净。
为了简单方便的干净删除控件,我提供了invalidate方法:

- (void)invalidate
{
    [self ex_removeFromSuperview];
    [[self backgroundView] removeFromSuperview];
    UIViewController *VC = [self VC];
    [[VC navigationItem] setTitleView:nil];
    [self setBlock:nil];
    [self setVC:nil];
    [self setBackgroundView:nil];
    [self setTitleLabel:nil];
    [self setImageView:nil];
    
    return;
}

通过+ load方法实现方法交换的载入。
通过方法交换,把removeFromSuperview给交换掉,防止外界调用removeFromSuperview。

留下的坑

  • 目前不支持pods集成
  • 检测是否显示tabar时,目前只支持tabbarController作为rootController的情况,对于UINavigationController里的tabbarController、侧滑 + tabbarController、侧滑 + UINavigationController这3种情况暂时无法判断,如果遮罩出现到tabbar下面时,你可以设置layoutGuideBottom来解决这个问题
  • 暂时不支持你自己写的cell,如果你需要自定义cell,请继承ZTMenuCell,在createView中创建view,实现selecteButton和resetButton方法,分别用来设置cell的选中状态和清除选中状态;如果不满足于此的话,修改源码吧

iOS OC Swift Flutter开发群 139322447

你可能感兴趣的:(【iOS】3行代码轻松集成导航栏菜单--ZTNavigationMenu)