iOS开发精华笔记 | 从封装一个菜单栏谈如何正确的封装一个控件

iOS开发精华笔记 | 从封装一个菜单栏谈如何正确的封装一个控件_第1张图片
iu

一. 写在前面

自定义控件可以说是每一个iOS开发者日常生活的一部分,每当拿到一张UI图我们都会观察里面相似的元素,思考是否需要封装。

二. 是否需要封装?

首先明确:封装的最终目的是复用。

先看一张设计图:


iOS开发精华笔记 | 从封装一个菜单栏谈如何正确的封装一个控件_第2张图片

可以看到,菜单栏在多个页面出现,并且长得差不多,所以我们想都不用想就决定封装了(专业的说法是:为了更好的复用代码、提升内聚度,这里应该封装)。


菜单栏

三. 如何封装?

封装的原则:把尽可能多的东西藏起来,对外提供简捷的接口。

四. 开始封装

1. 对需求做全方位分析:

  • 设计图中按钮只有两个或三个,实际会不会更多?如果可以更多,按钮很多时是不是让菜单栏支持左右滑动?(PM答:只有两个或三个)
  • 按钮文本右上角的角标为0时隐藏?角标大于99时显示99+还是确切数字?(PM答:显示确切的)
  • 按钮下方的红线宽度是固定的还是与文本同宽的?(PM答:和文本同宽)
  • 等等。。。(PM一一作答)

这些问题需要我们在动手开发之前就弄清楚,而不是带着问题去开发,更不是想当然的去开发。多跟产品沟通有时可以减少许多不必要的误会。

2. 对功能做全方位分析:

  • 按钮被点击后,被点击的按钮处于选中状态,其它按钮处于一般状态。
  • 按钮被点击后,红线移到被点击按钮正下方,宽度与文本宽度一致。
  • 按钮被点击后,菜单栏下方的scrollView也要进行相应调整。
  • 按钮可以展示\隐藏角标。

3. 磨刀不误砍柴工,弄清上面两个问题后再开始封装:
先封装带角标的按钮:由于角标是在按钮的titleLabel右上角,而titleLabel的宽度会随着title的改变而改变,要跟这种frame不固定的控件产生紧密关联,建议使用自动布局。不使用自动布局就重写layoutSubviews方法。这里我使用了masonry。

  • 带角标的按钮 .h文件:
@interface BadgeButton : UIButton

/**
 显示角标
 
 @param badgeNumber 角标数量
 */
- (void)showBadgeWithNumber:(NSInteger)badgeNumber;

/** 隐藏角标 */
- (void)hideBadge;

@end

  • 带角标的按钮 .m文件:
@interface BadgeButton ()

/** 显示按钮角标的label */
@property (nonatomic,strong) UILabel *badgeLabel;

@end

@implementation BadgeButton

#pragma mark - 构造方法
- (instancetype)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        // button属性设置
        self.clipsToBounds = NO;
    
        //------- 角标label -------//
        self.badgeLabel = [[UILabel alloc]init];
        [self addSubview:self.badgeLabel];
        self.badgeLabel.backgroundColor = [UIColor colorWithHexString:@"d51619"];
        self.badgeLabel.font = [UIFont systemFontOfSize:10];
        self.badgeLabel.textColor = [UIColor whiteColor];
        self.badgeLabel.layer.cornerRadius = 6;
        self.badgeLabel.clipsToBounds = YES;
        
        //------- 建立角标label的约束 -------//
        [self.badgeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.mas_equalTo(self.titleLabel.mas_right).mas_offset(-3);
            make.bottom.mas_equalTo(self.titleLabel.mas_top).mas_offset(8);
            make.height.mas_equalTo(12);
        }];
    }
    return self;
}

#pragma mark - 显示角标
/**
 显示角标

 @param badgeNumber 角标数量
 */
- (void)showBadgeWithNumber:(NSInteger)badgeNumber{
    self.badgeLabel.hidden = NO;
    // 注意数字前后各留一个空格,不然太紧凑
    self.badgeLabel.text = [NSString stringWithFormat:@" %ld ",badgeNumber];
}

#pragma mark - 隐藏角标
/** 隐藏角标 */
- (void)hideBadge{
    self.badgeLabel.hidden = YES;
}

#pragma mark - 设置按钮的选中状态
/** 设置按钮的选中状态 */
- (void)setSelected:(BOOL)selected{
    if (selected) {
        [self setTitleColor:[UIColor colorWithHexString:@"d51619"] forState:UIControlStateNormal];
        [self.titleLabel setFont:[UIFont boldSystemFontOfSize:15]];
    }else{
        [self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        [self.titleLabel setFont:[UIFont systemFontOfSize:15]];
    }
}

@end

接着封装菜单栏:

  • 菜单栏 .h文件:
#import 

@class BaseMenuView;

@protocol BaseMenuViewDelegate 

/** 菜单view的第几个button被点击(从0开始) */
- (void)menuView:(BaseMenuView *)menuView didClickButtonAtIndex:(NSInteger)index;

@end


@interface BaseMenuView : UIView

/** 标题数组 */
@property (nonatomic,strong) NSArray *titleArray;

@property (nonatomic,weak) id delegate;

/** 选中第几个按钮(从0开始) */
- (void)selectButtonAtButtonIndex:(NSInteger)buttonIndex;

/**
 显示按钮的角标
 
 @param badge 角标数
 @param buttonIndex 第几个button
 */
- (void)showButtonBadge:(NSInteger)badge atButtonIndex:(NSInteger)buttonIndex;

@end
  • 菜单栏 .m文件:
#import "BaseMenuView.h"
#import "BadgeButton.h"

/** 起始按钮的tag值 */
const NSInteger Button_Begin_Tag = 100;

@interface BaseMenuView (){
    /** 底部会移动的红色线 */
    UIView *_redView;
}

@end

@implementation BaseMenuView

#pragma mark - 构造方法
- (instancetype)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        // UI搭建
        [self setUpUI];
    }
    return self;
}

#pragma mark - UI搭建
/** UI搭建 */
- (void)setUpUI{
    self.backgroundColor = [UIColor whiteColor];
    
    //------- 创建红色线 -------//
    _redView = [[UIView alloc]initWithFrame:CGRectMake(0, self.height - 2, 0, 2)];
    [self addSubview:_redView];
    _redView.backgroundColor = [UIColor colorWithHexString:@"d51619"];
}

#pragma mark - 设置菜单的标题
/** 设置菜单的标题 */
- (void)setTitleArray:(NSArray *)titleArray{
    
    _titleArray = titleArray;
    
    //------- 先将已有的button全部移除 -------//
    for (BadgeButton *button in self.subviews) {
        if ([button isMemberOfClass:[BadgeButton class]]) {
            [button removeFromSuperview];
        }
    }
    
    //------- 再依次创建button -------//
    for (int i = 0; i < _titleArray.count; i ++) {
        CGFloat buttonWidth = SCREEN_WIDTH / _titleArray.count; // 按钮宽
        BadgeButton *button = [[BadgeButton alloc]initWithFrame:CGRectMake(i * buttonWidth, 0, buttonWidth, self.height - 2)];
        [self addSubview:button];
        button.selected = NO;
        [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchDown];
        button.tag = Button_Begin_Tag + i;
        [button setTitle:_titleArray[i] forState:UIControlStateNormal];
        [button layoutIfNeeded];
    }
    
}

#pragma mark - 按钮点击
/** 按钮点击 */
- (void)buttonClicked:(BadgeButton *)sender{
    NSInteger index = sender.tag - Button_Begin_Tag;
    // 改变按钮状态
    [self selectButtonAtButtonIndex:index];
    // 代理方执行相应方法
    if ([self.delegate respondsToSelector:@selector(menuView:didClickButtonAtIndex:)]) {
        [self.delegate menuView:self didClickButtonAtIndex:index];
    }
}

#pragma mark - 第几个按钮被选中(只改变UI,不触发代理方法)
/** 第几个按钮被选中(只改变UI,不触发代理方法) */
- (void)selectButtonAtButtonIndex:(NSInteger)buttonIndex{
    
    // 遍历所有button
    for (BadgeButton *button in self.subviews) {
        if ([button isMemberOfClass:[BadgeButton class]]) {
            button.selected = NO;
        }
    }
    
    // 获取到当前被点击的button
    BadgeButton *clickedButton = [self viewWithTag:(buttonIndex + Button_Begin_Tag)];
    clickedButton.selected = YES;
    
    // 底部红色线移到被点按钮下方
    [UIView animateWithDuration:0.3 animations:^{
        _redView.width = clickedButton.titleLabel.width;
        _redView.centerX = clickedButton.centerX;
    }];
}

#pragma mark - 显示按钮的角标
/**
 显示按钮的角标
 
 @param badge 角标数
 @param buttonIndex 第几个button
 */
- (void)showButtonBadge:(NSInteger)badge atButtonIndex:(NSInteger)buttonIndex{
    BadgeButton *button = [self viewWithTag:(buttonIndex + Button_Begin_Tag)];
    [button showBadgeWithNumber:badge];
}

@end

五. 使用这个控件

1. 引入delegate


2. 设置属性及确定代理方:

    //------- 菜单栏 -------//
    self.menuView = [[MainViewControllerMenuView alloc]initWithFrame:CGRectMake(0, self.naviView.maxY, SCREEN_WIDTH, 30)];
    self.menuView.titleArray = @[@"待取货",@"配送中",@"配送完成"];
    [self.view addSubview:self.menuView];
    self.menuView.delegate = self;
    [self.menuView selectButtonAtButtonIndex:0]; // 默认“待取货”

3. 处理代理方法

#pragma mark - 自定义控件的代理方法
#pragma mark -- 菜单栏的代理方法
- (void)menuView:(BaseMenuView *)menuView didClickButtonAtIndex:(NSInteger)index{
    [self scrollToPage:index];
}

六. 注意事项&细节

1. protocol的命名:

@protocol BaseMenuViewDelegate 

看看官方是怎么命名的:
UITableViewDelegate
UIScrollViewDelegate
命名方式:类名+Delegate

2. 代理方法的命名:

@protocol BaseMenuViewDelegate 

/** 菜单view的第几个button被点击(从0开始) */
- (void)menuView:(BaseMenuView *)menuView didClickButtonAtIndex:(NSInteger)index;

@end

看看官方是怎么命名的:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

- (void)scrollViewDidScroll:(UIScrollView *)scrollView;

命名方式:某个控件 什么事件

如果不知道该如何命名,查看官方文档或是想想官方是怎样给系统方法命名的。

七. 总结

1. 封装前弄清需求不留疑惑
2. 理清思路再动手
3. 严格要求代码规范
4. 写清楚注释
5. 让小伙伴code review


demo地址:

点此获取demo


最后,开车

iOS开发精华笔记 | 从封装一个菜单栏谈如何正确的封装一个控件_第3张图片
你的每一句代码都能在不经意间反映出你的水平

你可能感兴趣的:(iOS开发精华笔记 | 从封装一个菜单栏谈如何正确的封装一个控件)