
一. 写在前面
自定义控件可以说是每一个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 <UIKit/UIKit.h>
@class BaseMenuView;
@protocol BaseMenuViewDelegate <NSObject>
/** 菜单view的第几个button被点击(从0开始) */
- (void)menuView:(BaseMenuView *)menuView didClickButtonAtIndex:(NSInteger)index;
@end
@interface BaseMenuView : UIView
/** 标题数组 */
@property (nonatomic,strong) NSArray *titleArray;
@property (nonatomic,weak) id<BaseMenuViewDelegate> 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
<BaseMenuViewDelegate>
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 <NSObject>
看看官方是怎么命名的:
UITableViewDelegate
UIScrollViewDelegate
命名方式:类名+Delegate
2. 代理方法的命名:
@protocol BaseMenuViewDelegate <NSObject>
/** 菜单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地址:
最后,开车

网友评论
功能大部分人会写,封装讲真,写了几年代码的不一定会
其次
_redView.width = clickedButton.titleLabel.width真的没问题么?
推荐使用YYKit button.intrinsicContentSize.width
I resolved it. App run on iOS8, build by Xcode 6, not update frame right after UIButton setTitle, setTitleEdgeInsets. It will wait for next UI update. So, if you get frame of titleLabel, you will get CGRectZero.
Solution:
Call below methods after set UI property, to update layout immediately:
[self setNeedsLayout];
[self layoutIfNeeded];
Or:
Delay a second, and you can get titleFrame, use self.titleLabel.frame.
Or:
dispatch_async in main queue, to move code to next queue
上面这段粘自http://stackoverflow.com/questions/25758599/uibutton-titlelabel-frame-size-returning-cgsize-with-zero-width-height#
可以看一下
具体为什么,讲真我也不懂/(ㄒoㄒ)/~~
还有,我如果使用延迟加载选中按钮,按钮下也是有红线的:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// UI搭建
[self setUpUI];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 0.1秒后异步执行这里的代码...
// 默认选中第二个按钮
[self.menuView selectButtonAtButtonIndex:1];
self.contentScrollView.contentOffset = CGPointMake(SCREEN_WIDTH, 0);
});
}
希望前辈指点一二,小生先行道谢。
哈哈