美文网首页实用小功能fish的iOSiOS学习专题
iOS开发精华笔记 | 从封装一个菜单栏谈如何正确的封装一个控件

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

作者: 无夜之星辰 | 来源:发表于2017-04-01 00:58 被阅读1234次
iu

一. 写在前面

自定义控件可以说是每一个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地址:

点此获取demo


最后,开车

你的每一句代码都能在不经意间反映出你的水平

相关文章

网友评论

  • 贱精先玍丶:使用怎么使用? 菜鸟一个, 第五步开始有点看不懂?
    无夜之星辰:@贱精先玍丶 小意思:sunglasses:
    贱精先玍丶:@无夜之星辰 好的,谢谢:relaxed:
    无夜之星辰:就像用UITableView一样,引入代理,确立代理方,处理代理方法。可以看看demo:https://github.com/wyzxc/menuView/tree/master
  • helloDolin:首先给博主点个赞哈

    功能大部分人会写,封装讲真,写了几年代码的不一定会

    其次

    _redView.width = clickedButton.titleLabel.width真的没问题么?

    推荐使用YYKit button.intrinsicContentSize.width
    helloDolin:@无夜之星辰 前辈不敢当,我也是菜鸟

    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ㄒ)/~~
    无夜之星辰:@helloDolin 学习了!:smile: 能否解释一下为何clickedButton.titleLabel.width为何开始是0?我如果改成_redView.width = clickedButton.width就OK

    还有,我如果使用延迟加载选中按钮,按钮下也是有红线的:
    - (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);
    });

    }

    希望前辈指点一二,小生先行道谢。:smile:
    推遍天下无敌手:稳啊,我的哥
  • 洁简:加个demo更明了。。
    无夜之星辰:demo来了:https://github.com/wyzxc/menuView/tree/master
    :smile:
  • 推遍天下无敌手:_redView.width = clickedButton.titleLabel.width这个在初始化的时候的值是0啊楼主
    无夜之星辰:@推遍天下无敌手 确实:smile:
    推遍天下无敌手:@无夜之星辰 9楼稳...
    无夜之星辰:@推遍天下无敌手 初始化后根据需要调用选中某个按钮方法
  • d920e665d3d1:已收藏,还没封装过,看了正好有思路
    无夜之星辰:@推遍天下无敌手 有demo了:https://github.com/wyzxc/menuView/tree/master
    哈哈:smile:
    推遍天下无敌手:_redView.width = clickedButton.titleLabel.width,这个在进入的时候的值为0啊楼主
    无夜之星辰:@蒋俊杰 :smile:
  • moonCoder:小姐姐真漂亮
    无夜之星辰:@moonCoder 应该说小妹妹哦:joy:
  • Origheart:配图不错
    无夜之星辰:@Origheart :sweat_smile:
  • Arthurcsh:配图不错
    Arthurcsh:@无夜之星辰 有个demo更生动了
    无夜之星辰:@Arthurcsh 还可以:sunglasses:
  • 伦敦乡下的小作家:开车~~~
    无夜之星辰:@伦敦乡下的小作家 🚗

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

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