驗證碼倒計時按鈕、登錄注冊模塊封裝

1.倒計時按鈕封裝

使用場景:注冊1頁點擊獲取驗證碼按鈕,push到注冊2頁。界面如下“注冊2-1頁”所示,導航欄右按鈕馬上進入倒計時狀態并不可點擊;倒計時結束變成“注冊2-2頁”所示,可點擊并重新發送驗證碼。在做重置密碼功能的時候也用到了類似的邏輯。


注冊1頁
注冊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];
}];
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容

  • 之前要做一個發送短信驗證碼的倒計時功能,打算用NSTimer來實現,做的過程中發現坑還是有不少的。 基本使用 NS...
    WeiHing閱讀 4,394評論 1 8
  • 1.自定義控件 a.繼承某個控件 b.重寫initWithFrame方法可以設置一些它的屬性 c.在layouts...
    圍繞的城閱讀 3,421評論 2 4
  • iOS 的倒計時有多種實現細節,Cocoa Touch 為我們提供了 NSTimer 類和 GCD 的dispat...
    waylen閱讀 6,686評論 36 44
  • 1,NSObject中description屬性的意義,它可以重寫嗎?答案:每當 NSLog(@"")函數中出現 ...
    eightzg閱讀 4,163評論 2 19
  • 今天的話題是什么決定了時間的價格。我本來思考是自身創造的價值決定了價格,但笑來老師給出的答案是滿足用戶的需求決定了...
    hester_1982閱讀 541評論 0 1