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];
}];