验证码倒计时按钮、登录注册模块封装

1.倒计时按钮封装

使用场景:注册1页点击获取验证码按钮,push到注册2页。界面如下“注册2-1页”所示,导航栏右按钮马上进入倒计时状态并不可点击;倒计时结束变成“注册2-2页”所示,可点击并重新发送验证码。在做重置密码功能的时候也用到了类似的逻辑。


验证码倒计时按钮、登录注册模块封装_第1张图片
注册1页
验证码倒计时按钮、登录注册模块封装_第2张图片
注册2-1页
验证码倒计时按钮、登录注册模块封装_第3张图片
注2-2页

倒计时按钮的封装网上一抓一大把代码,也不会很复杂,那就根据自己项目需要封装一个吧!

按钮继承自UIButton,选择NSTimer作为定时器,在子线程中计时,主线程中修改ui。直接上代码:

因为用的是NSTimer,所以要注意强引用引起的内存问题。利用NSTimer分类作为timer的target来解除强引用,之前的一篇文章里面已经写过了所以就不多说了NSTimer的坑

#import "NSTimer+Addition.h"

@implementation NSTimer (Addition)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)(NSTimer *timer))block repeats:(BOOL)repeats{
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(blockInvoke:)
                                       userInfo:[block copy]
                                        repeats:repeats];
}

+ (void)blockInvoke:(NSTimer *)timer {
    void (^block)() = timer.userInfo;
    if(block) {
        block(timer);
    }
}
@end

倒计时按钮对外暴露的接口:

#import 

typedef void (^networkBlock)(void);//网络操作的block

@interface TimerButton : UIButton
@property (nonatomic ,weak) NSTimer *timer;
@property (nonatomic ,assign)CFRunLoopRef runloop;
//参数1 frame ;参数2 定时器计数次数;参数3 定时器计数间隔 ;参数4 :网络操作block
- (instancetype)initWithFrame:(CGRect)frame timerCount:(int)count timerInerval:(CGFloat)interval networkRequest:(networkBlock)networkBlock;

@end

按钮的具体实现:
1.初始化方法中做的:按钮的一些样式设置、计数次数等参数的赋值、开启timer(因为从注册1页push到下一页,timer就开始倒计时了,所以把timer的开启也放在初始化里做)
2.开启timer:使用gcd子线程中创建timer,因为NSTimer的定时器要添加到runloop才有效,所以要开启子线程runloop且runloop mode要适配。在timer触发时候执行的方法中做按钮UI的更新,如果计时完毕的话就销毁timer并且关闭runloop。
3.当倒计时完毕,按钮恢复可点击状态。点击按钮,发起网络请求获得验证码,并且创建新的timer

#import "TimerButton.h"
#import "NSTimer+Addition.h"
@interface TimerButton()
@property (nonatomic ,copy) networkBlock networkBlock;
@end

@implementation TimerButton
{
    int timerCount;
    int resetCount;
    CGFloat timerInterval;
}
//必须要在vc的dealloc方法中调用btn 的timer销毁方法和runloop的退出方法,保证vc pop的时候btn可以马上销毁

- (instancetype)initWithFrame:(CGRect)frame timerCount:(int)count timerInerval:(CGFloat)interval networkRequest:(networkBlock)networkBlock{
    if (self = [super initWithFrame:frame]) {
        
        timerCount = count;
        timerInterval = interval;
        self.networkBlock = [networkBlock copy];
        
        self.enabled = NO;
        [self setTitle:@"重发验证码" forState:UIControlStateNormal];
        [self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        [self addTarget:self action:@selector(btnClicked) forControlEvents:UIControlEventTouchUpInside];
        [self timerAction];
    }
    return self;
}

//点击按钮,如果有网络操作就执行网络操作,并且开启新的timer
- (void)btnClicked{
    if (self.networkBlock) {
        self.networkBlock();
    }
    [self timerAction];
}

//开启timer
- (void)timerAction{
    resetCount = timerCount;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        __weak typeof (self)weakself = self;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:timerInterval block:^(NSTimer *timer) {
            NSLog(@"。。。");
            __strong typeof(weakself) strongself = weakself;
            resetCount --;
            if (resetCount == 0) {
                [strongself.timer invalidate];
                strongself.enabled = YES;
                CFRunLoopStop(CFRunLoopGetCurrent());//这一句照理说其实也可以不写,因为定时器触发唤醒runloop,销毁timer,然后runloop判断还有没有源。因为没有源了,所以runloop会退出。
            }else{
                self.enabled = NO;
                dispatch_async(dispatch_get_main_queue(), ^{
                    [strongself setTitle:[NSString stringWithFormat:@"%ds后重发",resetCount] forState:UIControlStateDisabled];
                    [strongself setTitleColor:[UIColor grayColor] forState:UIControlStateDisabled];
                });
            }
        } repeats:YES];
        [self.timer fire];//马上执行
        self.runloop = CFRunLoopGetCurrent();
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    });
}

使用:
必须要在vc的dealloc方法中调用倒计时按钮的timer销毁方法和runloop的退出方法,保证vc pop的时候btn可以马上销毁。

@property (nonatomic ,weak) TimerButton *btn;

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    TimerButton *btn = [[TimerButton alloc]initWithFrame:CGRectMake(0, 0, 100, 40) timerCount:5 timerInerval:1.0 networkRequest:nil];
    _btn = btn;
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]initWithCustomView:btn];
}

- (void)dealloc{
    NSLog(@"vc销毁了");
    [self.btn.timer invalidate];
    CFRunLoopStop(self.btn.runloop);
}

ps:如果不写CFRunLoopStop(self.btn.runloop);,pop viewController时倒计时按钮无法释放(在倒计时按钮的类中写dealloc,pop viewController,按钮的dealloc方法没被调用,但控制器的dealloc方法调用了,所以控制器释放了而按钮没释放)。

为什么会这样请看:NSTimer的坑。主要是iOS10在处理子线程runloop上有所不同。例子中涉及到线程异步的问题,定时器是在子线程RunLoop中注册的,但定时器的移除操作却是在主线程,由于子线程RunLoop处理完一次定时信号后,就会进入休眠状态。在iOS10以前的环境下,定时器被移除后,内核仍然会向对应的Timer Port发送一次信号,所以子线程RunLoop接收到信号后会被唤醒,由于没有定时源需要处理,所以RunLoop会直接跳转到判断阶段,判断阶段会检测当前RunLoopMode是否有事件源需要处理,若没有事件源需要处理,则会退出RunLoop。
但在iOS10环境下,当定时器被移除后,内核不再向对应的Timer Port发送任何信号,所以子线程RunLoop一直处于休眠状态并没有退出,而我们只需要手动唤醒RunLoop(或者直接退出runloop)即可。

2.登录注册模块封装

项目里遇到这样一个需求,有一些功能是需要先登录然后才能使用的。当触发这些功能时,需要先判断用户是否已经登录。
1.未登录\已登录情况下,触发不需要登录的功能,直接跳转。
2.未登录情况下,触发需要登录的功能,先进入登录界面,登录成功则跳转,不成功或者取消登录就留在原页面。
3.已登录,触发需要登录的功能,直接跳转。

触发登录的“入口”有可能是按钮,也有可能是其他任何控件,所以单独写了一个LoginManager的类来管理,在需要引导登录的地方调用这个类的方法就能实现相应的引导“行为”。

思路:
1.在AppDelegate中用一个全局变量记录是否已经登录。在开启app时会先进行自动登录,并对这个全局变量进行赋值。

AppDelegate.h
@property (nonatomic ,assign) BOOL isLogin;

2.是否需要检查登录?不用检查登录、要检查登录但已经登录的情况就转跳到要去的功能界面。
3.需要检查登录而未登录,实例化LoginViewController,然后获取topMost presenting viewcontroller,present loginVC。

对外暴露的接口:

#import 
#import 

typedef void (^loginedBlock)(void);

static NSString * const HXPushViewControllerNotification = @"hxPushViewController";
static NSString * const HXDismissViewControllerNotification = @"hxDismissViewController";

@interface LoginManager : NSObject
//参数1:触发登录时 最顶层的视图控制器 ;参数2:是否需要检查登录 ;参数3:已经登录、不需检查登录时要执行的block
+ (BOOL)checkLoginWithTopPresentingViewControllre:(UIViewController *)viewcontroller isCheckLogin:(BOOL)check loginedBlock:(loginedBlock)loginedBlock;

@end

+ (BOOL)checkLoginWithTopPresentingViewControllre:(UIViewController *)viewcontroller isCheckLogin:(BOOL)check loginedBlock:(loginedBlock)loginedBlock;方法参数的含义:
viewcontroller:顶层 presenting viewcontroller
check:是否检查登录
loginedBlock:已经登录、不需检查登录时的操作

具体实现:

#import "LoginManager.h"
#import "AppDelegate.h"
#import "NeedLoginViewController.h"

@interface LoginManager()
@property (nonatomic ,strong)UIViewController *topPresentingViewController;
@property (nonatomic ,copy)loginedBlock loginedBlock;
@end

@implementation LoginManager
static LoginManager *_instance;

+ (instancetype)shareLoginManager{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[super allocWithZone:NULL] init];
    });
    return _instance;
}

+ (BOOL)checkLoginWithTopPresentingViewControllre:(UIViewController *)viewcontroller isCheckLogin:(BOOL)check loginedBlock:(loginedBlock)loginedBlock{
    LoginManager *manager = [LoginManager shareLoginManager];
    return [manager checkLoginWithTopPresentingViewControllre:viewcontroller isCheckLogin:check loginedBlock:loginedBlock];
}

- (BOOL)checkLoginWithTopPresentingViewControllre:(UIViewController *)viewcontroller isCheckLogin:(BOOL)check loginedBlock:(loginedBlock)loginedBlock{
    self.topPresentingViewController = viewcontroller;
    self.loginedBlock = [loginedBlock copy];
    //要检查是否已经登录
    if (check) {
        AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
        //已登录
        if (appDelegate.isLogin) {
            if (self.loginedBlock) {
                self.loginedBlock();
            }
            return YES;
        }
        //未登录
        else{
            [self presentLoginPage];
            return NO;
        }
    }
    //不检查登录
    else{
        if (self.loginedBlock) {
            self.loginedBlock();
        }
        return YES;
    }
}

- (void)presentLoginPage{
    //通知添加。先移除再添加.否则在登录界面点取消,再触发登录检查时会再次来到这个方法,导致多次添加通知。
    [[NSNotificationCenter defaultCenter] removeObserver:self name:HXPushViewControllerNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pushVC:) name:HXPushViewControllerNotification object:nil];
    //实例化loginVC 获取顶层VC,present loginVC
    NeedLoginViewController *nLoginVC = [[NeedLoginViewController alloc]init];
    UINavigationController *navi = [[UINavigationController alloc]initWithRootViewController:nLoginVC];
    [self.topPresentingViewController presentViewController:navi animated:YES completion:^{
    }];
}

//一般是登录成功后post HXPushViewControllerNotification
- (void)pushVC:(NSNotification *)notification{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:HXPushViewControllerNotification object:nil];
    self.loginedBlock();
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    appDelegate.isLogin = YES;
}

- (void)dismissVC:(NSNotification *)notification{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:HXDismissViewControllerNotification object:nil];
}

- (void)dealloc{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:HXPushViewControllerNotification object:nil];
}
@end

presentLoginPage方法这里注册了个通知。登录成功后在登录界面post该通知,然后执行相应的通知方法跳转到下一个界面中去,在这里使用的是已经登录、不需检查登录时的loginedBlock。
ps:这个通知先移除,再添加的原因:登录界面点“取消登录”,再触发登录检查时会再次来到这个方法,导致多次添加通知。

使用:
比如我们在点击tabbar的第二个tab时会触发登录检查:

- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController{
    if (viewController.tabBarItem.tag == 1 ) {
        return [LoginManager checkLoginWithTopPresentingViewControllre:tabBarController isCheckLogin:YES loginedBlock:^{
            //已经登录、或者未登录但在present 的登录界面中登录成功就会执行这个block
            tabBarController.selectedIndex = 1;
        }];
    }else{
        return YES;
    }
}

登录界面:
登录成功

- (void)loginBtn{
    ......
    //登录成功!
    [self dismissViewControllerAnimated:YES completion:^{
        [[NSNotificationCenter defaultCenter] postNotificationName:HXPushViewControllerNotification object:nil];
    }];
    ......
}

取消登录:直接dismiss viewcontroller

3.其他

后台返回的json中的boolean类型数据。开始以为和OC里的BOOL类型是同一回事,但后来发现怎么都不对于是打断点发现是__NSCFBoolean类型。

NSCFBoolean是NSNumber类簇中的一个私有的类。它是通往CFBooleanRef类型的桥梁,它被用来给Core Foundation的属性列表和集合封装布尔数值。CFBoolean定义了常量kCFBooleanTrue和kCFBooleanFalse。因为CFNumberRef和CFBooleanRef在Core Foundation中属于不同种类,这样是有道理的,它们在NSNumber被以不同的衔接类呈现。

转换成BOOL调用boolValue方法:[nscfBooleanValue boolValue];

更新:模态视图释放不当造成内存泄露
在做倒计时按钮时遇到一个比较诡异的事情。
做登录注册功能时的模态视图用到了导航栏,按流程走一步步填写信息并且push到下一步,当流程走完要dismiss掉整个模态视图。
VC -> present A(嵌套NaviagtionController) -> push B(B的导航栏右按钮是封装的倒计时按钮) -> push C -> push D -> dismiss VC。
尽管在写demo测试时倒计时按钮不会有内存泄漏问题,但因为用到了NSTimer,怕有内存泄漏就还是在按钮类里写了dealloc。dismiss时,按钮的dealloc没有调用。然后给A、B、C、D控制器都写了dealloc,发现控制器的dealloc都调用了。但如果是从B pop回到A,按钮的dealloc又可以调用到。
按钮的dealloc没有调用到而控制器的dealloc调用了,那是按钮的内存泄漏了。找了很久才发现问题不是出在封装的按钮身上。我写了另外一个按钮:继承自UIButton,然后里面只有一个dealloc方法,把它放到C的导航栏上,dismiss时同样也不会调用到dealloc.

@implementation HXBtnTest 
- (void)dealloc{
    NSLog(@"btn销毁了");
}
@end

present A:

AViewController *aVC = [[AViewController alloc]init];
UINavigationController *navi = [[UINavigationController alloc]initWithRootViewController:aVC];
tabBarController presentViewController:navi animated:YES completion:nil];

在D中的dismiss是这样写的:

[self dismissViewControllerAnimated:YES completion:^{
    [[NSNotificationCenter defaultCenter] postNotificationName:HXPushViewControllerNotification object:nil];
}];

基本上一直以来都是这样写代码,没意识过会有问题,上网查似乎又没有人问过类似的问题。。

既然导航栏上的按钮没有被释放,那么久证明还有别的东西在强引用着它。按钮在导航栏上,那么强引用的就是navigationController了,而且情况是,嵌套在nav vc中的视图控制器都释放了而nav vc没有释放。

关于导航栏,准确来说应该是这样的:
navigationcontroller直接控制viewcontrollers集合,然后它包含的navigationbar是整个工程的导航栏,bar有一个用来管理navigationItem的栈。@property(nonatomic, copy) NSArray *items
navigationItem包含了navigationbar视图的全部元素(如title,tileview,backBarButtonItem等),每个视图控制器的导航项元素由所在视图控制器的navigationItem管理。即设置当前页面的左右barbutton。

因此出现导航栏自定义按钮不能释放的问题有可能是因为navigationcontroller不正常pop造成的。比如当我们写self.navigationViewController popViewController:xxx 时,每pop一个视图控制器,对应的navigationItem 也会pop出栈,其管理的控件也得以释放。

所以这就解释了为什么在D VC中直接写self dismissViewControllerxxx不能释放导航栏按钮。如果你问我,navigationviewcontroller既然没被释放,那么它是被谁持有?我认为是present A的那个控制器。

如何修改:先popToRootViewController再dismiss

//先取得presentingViewController。不先保存的话,popvc之后可能就为空了
UIViewController *temp = self.presentingViewController;
[self.navigationController popToRootViewControllerAnimated:YES];
[temp dismissViewControllerAnimated:YES completion:^{
    [[NSNotificationCenter defaultCenter] postNotificationName:HXPushViewControllerNotification object:nil];
}];

你可能感兴趣的:(验证码倒计时按钮、登录注册模块封装)