1.倒计时按钮封装
使用场景:注册1页点击获取验证码按钮,push到注册2页。界面如下“注册2-1页”所示,导航栏右按钮马上进入倒计时状态并不可点击;倒计时结束变成“注册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 <UIKit/UIKit.h>
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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
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 <UINavigationItem *> *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];
}];