一. 写在前面
自定义控件可以说是每一个iOS开发者日常生活的一部分,每当拿到一张UI图我们都会观察里面相似的元素,思考是否需要封装。
二. 是否需要封装?
首先明确:封装的最终目的是复用。
先看一张设计图:
可以看到,菜单栏在多个页面出现,并且长得差不多,所以我们想都不用想就决定封装了(专业的说法是:为了更好的复用代码、提升内聚度,这里应该封装)。
三. 如何封装?
封装的原则:把尽可能多的东西藏起来,对外提供简捷的接口。
四. 开始封装
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