iOS开发实用技术IOS收藏iOS基础知识点

iOS开发造轮子 | 优雅的封装一个倒计时button

2017-09-09  本文已影响850人  Lol刀妹
iu

目标

封装一个满足基本功能并且方便使用的倒计时按钮,关键是:

要优雅

需要实现的基本功能

以获取短信验证码为例。用户点击获取验证码按钮后,按钮enabled立即设置为NO,并且向后台发送请求,若请求失败,恢复按钮的enabled,反之开始倒计时,期间持续刷新按钮文本,倒计时结束后重置按钮。

需要处理的几个点

1.用户点击按钮

此时按钮的enabled立即变为NO,并向后台发起请求。

2.请求结束

若请求成功,开始倒计时,反之恢复按钮的可点状态并提示用户重试。

3.倒计时进行中

持续刷新按钮文本。

4.倒计时结束

恢复按钮的可点状态,重置按钮文本。

倒计时按钮封装

将上述几个需要处理的点以block的方式封装:

#import "CQCountDownButton.h"
#import <MSWeakTimer.h>

typedef void(^ButtonClickedBlock)();
typedef void(^CountDownStartBlock)();
typedef void(^CountDownUnderwayBlock)(NSInteger restCountDownNum);
typedef void(^CountDownCompletionBlock)();

@interface CQCountDownButton ()

/** 控制倒计时的timer */
@property (nonatomic, strong) MSWeakTimer *timer;
/** 按钮点击事件的回调 */
@property (nonatomic, copy) ButtonClickedBlock buttonClickedBlock;
/** 倒计时开始时的回调 */
@property (nonatomic, copy) CountDownStartBlock countDownStartBlock;
/** 倒计时进行中的回调(每秒一次) */
@property (nonatomic, copy) CountDownUnderwayBlock countDownUnderwayBlock;
/** 倒计时完成时的回调 */
@property (nonatomic, copy) CountDownCompletionBlock countDownCompletionBlock;

@end

@implementation CQCountDownButton {
    /** 倒计时开始值 */
    NSInteger _startCountDownNum;
    /** 剩余倒计时的值 */
    NSInteger _restCountDownNum;
}

/**
 构造方法
 
 @param frame frame
 @param duration 倒计时时间
 @param buttonClicked 按钮点击事件的回调
 @param countDownStart 倒计时开始时的回调
 @param countDownUnderway 倒计时进行中的回调(每秒一次)
 @param countDownCompletion 倒计时完成时的回调
 @return 倒计时button
 */
- (instancetype)initWithFrame:(CGRect)frame
                     duration:(NSInteger)duration
                buttonClicked:(void(^)())buttonClicked
               countDownStart:(void(^)())countDownStart
            countDownUnderway:(void(^)(NSInteger restCountDownNum))countDownUnderway
          countDownCompletion:(void(^)())countDownCompletion {
    if (self = [super initWithFrame:frame]) {
        _startCountDownNum = duration;
        self.buttonClickedBlock       = buttonClicked;
        self.countDownStartBlock      = countDownStart;
        self.countDownUnderwayBlock   = countDownUnderway;
        self.countDownCompletionBlock = countDownCompletion;
        [self addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
    }
    return self;
}

/** 按钮点击 */
- (void)buttonClicked:(CQCountDownButton *)sender {
    sender.enabled = NO;
    self.buttonClickedBlock();
}

/** 开始倒计时 */
- (void)startCountDown {
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
    _restCountDownNum = _startCountDownNum;
    self.countDownStartBlock(); // 调用倒计时开始的block
    self.timer = [MSWeakTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(refreshButton) userInfo:nil repeats:YES dispatchQueue:dispatch_get_main_queue()];
}

/** 刷新按钮内容 */
- (void)refreshButton {
    _restCountDownNum --;
    self.countDownUnderwayBlock(_restCountDownNum); // 调用倒计时进行中的回调
    if (_restCountDownNum == 0) {
        [self.timer invalidate];
        self.timer = nil;
        _restCountDownNum = _startCountDownNum;
        self.countDownCompletionBlock(); // 调用倒计时完成的回调
        self.enabled = YES;
    }
}

畅快使用,一个方法搞定所有事件处理及回调

__weak __typeof__(self) weakSelf = self;

self.countDownButton = [[CQCountDownButton alloc] initWithFrame:CGRectMake(90, 90, 150, 30) duration:10 buttonClicked:^{
    //------- 按钮点击 -------//
    [SVProgressHUD showWithStatus:@"正在获取验证码..."];
    // 请求数据
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        int a = arc4random() % 2;
        if (a == 0) {
            // 获取成功
            [SVProgressHUD showSuccessWithStatus:@"验证码已发送"];
            // 获取到验证码后开始倒计时
            [weakSelf.countDownButton startCountDown];
        } else {
            // 获取失败
            [SVProgressHUD showErrorWithStatus:@"获取失败,请重试"];
            weakSelf.countDownButton.enabled = YES;
        }
    });
} countDownStart:^{
    //------- 倒计时开始 -------//
    NSLog(@"倒计时开始");
} countDownUnderway:^(NSInteger restCountDownNum) {
    //------- 倒计时进行中 -------//
    [weakSelf.countDownButton setTitle:[NSString stringWithFormat:@"再次获取(%ld秒)", restCountDownNum] forState:UIControlStateNormal];
} countDownCompletion:^{
    //------- 倒计时结束 -------//
    [weakSelf.countDownButton setTitle:@"点击获取验证码" forState:UIControlStateNormal];
    NSLog(@"倒计时结束");
}];

亮点

倒计时进行中的block,通过传递剩余倒计时数值,优雅实现按钮的持续更新:

countDownUnderway:^(NSInteger restCountDownNum) {
    //------- 倒计时进行中 -------//
    [weakSelf.countDownButton setTitle:[NSString stringWithFormat:@"再次获取(%ld秒)", restCountDownNum] forState:UIControlStateNormal];
 } 

Block or Delegate?

每当涉及到回调的时候我都会考虑这个问题。

block强调结果而delegate强调过程,在这里我们显然要的是结果。还有就是,如果这里采用delegate,代码的组织将更繁琐(需要4个代理方法依次对应4个block)。

但是,如果真的用delegate的话,用#pragma mark - count down将4个代理方法放在一起,想较block而言代码结构会更加的层次分明,这也是大家通常认为delegate更容易维护的原因之一吧。

使用block还是delegate这是一个仁者见仁智者见智的问题,我个人认为如果你对你的代码有较高要求、懂得换位思考,那么你不管使用delegate还是block都可以写出赏心悦目的代码,反之,都将惨不忍睹。

block和delegate的选择可以参考下这篇文章

内存管理

因为涉及到timer和block,所以内存这一块要警惕。

关于timer

MSWeakTimer 你值得拥有

Thread-safe NSTimer drop-in alternative that doesn't retain the target and supports being used with GCD queues.

关于block

注意使用weakSelf。

最重要的还是用instrument彻底的检查一下。
我已经用instrument检查多次了,请放心使用。

给dalao递优秀三方库.gif

demo

点击获取demo

期望

希望有大佬打赏一块钱买鸡蛋。


flow down the poor's tears

2017年10月7日更新

去掉构造方法中的frame参数以适配自动布局。
代码已更新到GitHub.


2018年1月26日更新

添加结束倒计时的方法:

/** 结束倒计时 */
- (void)endCountDown {
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
    self.enabled = YES;
    !self.countDownCompletionBlock ?: self.countDownCompletionBlock();
}

block调用时先判断是否为nil,如:

// 调用倒计时开始的block
!self.countDownStartBlock ?: self.countDownStartBlock(); 
// 调用倒计时完成的回调
!self.countDownCompletionBlock ?: self.countDownCompletionBlock(); 

代码已更新到GitHub。

上一篇下一篇

猜你喜欢

热点阅读