? 最近在做用戶登錄獲取驗證碼時添加圖形驗證碼功能,就是只有正確輸入圖形驗證碼才能收到后臺發送的短信驗證碼。效果如下:
?
? 看起來雖然是個小功能,但是實際操作起來,會發現蘋果給我們留下的坑,當然更多的是自己給自己挖的坑。本文就是解決這個小功能出現的最麻煩的一個坑。
? 先介紹圖中四個輸入框的業務邏輯:
? 1、每個輸入框都不能手動點擊成為第一響應者,只能通過鍵盤輸入控制,也就是只能前進和后退;
? 2、輸入框全部輸入且正確就請求后臺獲取相應手機的短信驗證碼,請求失敗則在視圖中顯示失敗信息;
?
? 主要的功能就這兩點,當然還有更細節的地方就不作考慮。然后在此過程中,發現了一個非常致命的坑:由于后臺給的圖形驗證碼是純數字的,然后我定義的四個輸入框限制了鍵盤類型都為數字鍵盤,然后問題是當輸入框沒有任何文字時,點擊鍵盤的delete(?)鍵沒有任何效果,從代碼的角度來講就是textField的代理和點擊事件中沒有一個方法能夠監聽到這個回調,那么我所要實現的輸入框無文字點擊delete鍵使后一個textField成為第一響應者的功能就變得毫無可能。真是自作孽不可活啊,當然有很多方法可以直接跳過這個bug,比如換一個輸入框的實現方式:只用一個textField,然后在textField視圖上疊加四個label或是別的能顯示文字的視圖。或者還是原來的實現方式,只不過要自定義數字鍵盤(不建議這么做,系統的鍵盤做了很多特殊處理,如鍵盤優先級、通知方法等等,自己實現會花很多功夫)。當然,不止這些方法可以實現這個功能,只是作為程序員的我怎么能后避過眼前的bug呢?就是這樣一個不服輸的精神終于讓我想到了一個驚為天人的實現技巧。
? 說明:本文bug只適用于系統數字鍵盤,普通鍵盤是完全不會出現的,其他鍵盤我未作測試,請看清本文意圖。
?
? 接下來就來說明技巧的實現方式:
? 該技巧的精髓是亦幻亦真,蒙蔽用戶的眼睛。
? 既然在textField無文字時無法監聽到數字鍵盤的delete鍵,那么我另辟蹊徑,始終讓textField有文字,但是也不顯示,那就是使用“ ”(一個空格字符串)來代替nil(空字符串)。當用戶每輸入一個數字,讓下一個textField獲取焦點,與此同時,給下一textField文字賦上空格字符串,那么該textField就同時具備了再次輸入和監聽鍵盤delete鍵的特性;當該textField點擊了delete鍵時,讓上一個textField獲取焦點,并給其文字賦上空格字符串。如此循環往復,就能完成多個textField的焦點切換。但與此同時產生的問題,下一個textField在沒有輸入之前就已經有了空格字符串,當輸入時,文字就不再居中而是往后偏移了一個字符的寬度。當然,這怎么能難得了我:原本的每個textField都是有焦點的光標閃動的,現在我讓此光標不可見,然后在輸入數字的同時將原來的“空格+數字”字符串替換為本次輸入的數字就可以了。
? 廢話有點多了,直接上代碼。
? 首先我新建了一個類,繼承UITextField,目的是攔截用戶點擊,是點擊變得不可響應。
//
// YTUnclickableTextField.m
// 分時租賃
//
// Created by chips on 17/3/27.
// Copyright ? 2017年 柯其譜. All rights reserved.
//#import "YTUnclickableTextField.h"NSString * const YTUnclickableTextFieldSpace = @" ";@implementation YTUnclickableTextField- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {return nil;
}- (BOOL)becomeFirstResponder {self.text = YTUnclickableTextFieldSpace;return [super becomeFirstResponder];
}@end
? 該類重寫了兩個系統方法:第一個用于攔截用戶點擊以此確認點擊的目標view,直接返回nil后該該textField的示例就無法響應點擊事件,但焦點依然可以代碼獲取。有人就說了,直接將enabled或userInteractionEnabled屬性設置為NO就可以了。我的回答是絕對不行,設置任何一個屬性為NO不僅會導致不能響應用戶點擊事件,而且textField的焦點都無法獲取,親測。第一個方法只是在每一個textField獲取焦點時給文本賦值為空格字符串,并將該字符串設為外部變量,好讓圖形驗證碼view作下一步判斷。
? 接下來是重頭,圖形驗證碼自定義view類:
//
// PicVerifyCodeView.m
// 分時租賃
//
// Created by chips on 17/3/24.
// Copyright ? 2017年 柯其譜. All rights reserved.
//#import "PicVerifyCodeView.h"
#import "YTUnclickableTextField.h"
#import "YTHttpTool.h"
#import "Masonry.h"static NSInteger const kPicVerifyCodeNumber = 4;@interface PicVerifyCodeView () <UITextFieldDelegate>/** 請求圖形驗證碼圖片的url字符串 */
@property (nonatomic, copy) NSString *imageUrlString;
/** 驗證碼錯誤label */
@property (nonatomic, strong) UILabel *errorLabel;
/** 圖形驗證碼imageView */
@property (nonatomic, strong) UIImageView *verifyCodeImageView;
/** 再生成圖形驗證碼button */
@property (nonatomic, strong) UIButton *regenerateButton;@end@implementation PicVerifyCodeView#pragma mark - setter and getter
- (NSMutableArray<YTUnclickableTextField *> *)textFields {if (_textFields == nil) {_textFields = [NSMutableArray array];}return _textFields;
}#pragma mark - Construction method
- (instancetype)initWithFrame:(CGRect)frame tel:(NSString *)tel delegate:(id<PicVerifyCodeViewDelegate>)delegate {if (self = [super initWithFrame:frame]) {self.backgroundColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.8];[self setupSubviews];self.imageUrlString = [NSString stringWithFormat:@"%@:%@/Account/ValidateCode?Tel=%@", YTHttpToolURLString, YTHttpToolPort, tel];[self generateVerCode];self.tel = tel;self.delegate = delegate;}return self;
}#pragma mark - Setup
- (void)setupSubviews {UIView *view = [[UIView alloc]init];[self addSubview:view];view.backgroundColor = [UIColor whiteColor];view.layer.cornerRadius = 10;CGFloat cancelImageViewW = 16;CGFloat margin = 16;UIImageView *cancelImageView = [[UIImageView alloc]init];[view addSubview:cancelImageView];cancelImageView.image = [UIImage imageNamed:@"chacha"];cancelImageView.userInteractionEnabled = YES;UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapCancelImageView:)];[cancelImageView addGestureRecognizer:tap];CGFloat labelH = 30;UILabel *label = [[UILabel alloc]init];[view addSubview:label];label.text = @"請輸入圖形驗證碼";label.textAlignment = NSTextAlignmentCenter;label.font = [UIFont systemFontOfSize:18];UILabel *errorLabel = [[UILabel alloc]init];[view addSubview:errorLabel];self.errorLabel = errorLabel;errorLabel.textColor = [UIColor redColor];errorLabel.textAlignment = NSTextAlignmentCenter;errorLabel.font = [UIFont systemFontOfSize:12];UIView *picView = [[UIView alloc]init];[view addSubview:picView];UIButton *button = [[UIButton alloc]init];self.regenerateButton = button;[view addSubview:button];button.backgroundColor = AppStyleColor;[button setImage:[UIImage imageNamed:@"sx"] forState:UIControlStateNormal];[button addTarget:self action:@selector(clickRegenerateButton) forControlEvents:UIControlEventTouchUpInside];UIImageView *picImageView = [[UIImageView alloc]init];self.verifyCodeImageView = picImageView;[picView addSubview:picImageView];UIView *textFieldsView = [[UIView alloc]init];[view addSubview:textFieldsView];for (int i = 0; i < kPicVerifyCodeNumber; i++) {YTUnclickableTextField *textField = [[YTUnclickableTextField alloc]init];[self.textFields addObject:textField];[textFieldsView addSubview:textField];textField.textAlignment = NSTextAlignmentCenter;textField.keyboardType = UIKeyboardTypeNumberPad;textField.tintColor = [UIColor clearColor];[[self class]setupBorderColor:textField];textField.layer.cornerRadius = 5;textField.layer.borderWidth = 1;textField.delegate = self;[textField addTarget:self action:@selector(editingChangedWith:) forControlEvents:UIControlEventEditingChanged];if (i == 0) {[textField becomeFirstResponder];textField.layer.borderColor = AppStyleColor.CGColor;}}[view mas_makeConstraints:^(MASConstraintMaker *make) {make.leading.equalTo(super.mas_leading).with.offset(40);make.top.equalTo(super.mas_top).with.offset(130);make.centerX.equalTo(super.mas_centerX);make.height.equalTo(view.mas_width).with.dividedBy(1.3);}];[cancelImageView mas_makeConstraints:^(MASConstraintMaker *make) {make.top.equalTo(view.mas_top).with.offset(margin);make.trailing.equalTo(view.mas_trailing).with.offset(-margin);make.width.mas_equalTo(cancelImageViewW);make.height.equalTo(cancelImageView.mas_width);}];[label mas_makeConstraints:^(MASConstraintMaker *make) {make.top.equalTo(cancelImageView.mas_bottom);make.leading.equalTo(view.mas_leading);make.trailing.equalTo(view.mas_trailing);make.height.mas_equalTo(labelH);}];[errorLabel mas_makeConstraints:^(MASConstraintMaker *make) {make.top.equalTo(label.mas_bottom);make.height.mas_equalTo(30);make.leading.equalTo(label.mas_leading);make.trailing.equalTo(label.mas_trailing);}];CGFloat picMargin = 16;[picView mas_makeConstraints:^(MASConstraintMaker *make) {make.top.equalTo(errorLabel.mas_bottom).with.offset(8);make.leading.equalTo(view.mas_leading).with.offset(cancelImageViewW+margin);make.trailing.equalTo(cancelImageView.mas_leading);make.bottom.equalTo(textFieldsView.mas_top).offset(-picMargin);}];[button mas_makeConstraints:^(MASConstraintMaker *make) {make.top.equalTo(picView.mas_top);make.trailing.equalTo(picView.mas_trailing);make.bottom.equalTo(picView.mas_bottom);make.width.equalTo(button.mas_height);}];[picImageView mas_makeConstraints:^(MASConstraintMaker *make) {make.top.equalTo(picView.mas_top);make.leading.equalTo(picView.mas_leading);make.trailing.equalTo(button.mas_leading);make.bottom.equalTo(picView.mas_bottom);}];[textFieldsView mas_makeConstraints:^(MASConstraintMaker *make) {make.height.equalTo(picView.mas_height);make.leading.equalTo(picView.mas_leading);make.trailing.equalTo(picView.mas_trailing);make.bottom.equalTo(view.mas_bottom).with.offset(-picMargin);}];CGFloat textFieldInset = 10;WeakSelf[self.textFields enumerateObjectsUsingBlock:^(YTUnclickableTextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) {[textField mas_makeConstraints:^(MASConstraintMaker *make) {make.top.equalTo(textFieldsView.mas_top);make.height.equalTo(textField.mas_width);if (idx == 0) {make.leading.equalTo(textFieldsView.mas_leading);make.trailing.equalTo(weakSelf.textFields[idx+1].mas_leading).with.offset(-textFieldInset);} else if (idx == self.textFields.count-1) {make.width.equalTo(weakSelf.textFields.firstObject.mas_width);make.trailing.equalTo(textFieldsView.mas_trailing);} else {make.width.equalTo(weakSelf.textFields.firstObject.mas_width);make.trailing.equalTo(weakSelf.textFields[idx+1].mas_leading).with.offset(-textFieldInset);}}];}];
}#pragma mark - Event response
- (void)tapCancelImageView:(UITapGestureRecognizer *)sender {[self removeFromSuperview];
}- (void)clickRegenerateButton {[self generateVerCode];
}- (void)editingChangedWith:(UITextField *)sender {if (![sender isFirstResponder]) {return;}if (!sender.text.length) {sender.text = YTUnclickableTextFieldSpace;} else {[self.textFields enumerateObjectsUsingBlock:^(YTUnclickableTextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) {if (sender == textField) {//最后一個輸入框獲取焦點if (idx == self.textFields.count-1) {//獲取完整的圖形驗證碼NSMutableString *verCode = [NSMutableString string];for (UITextField *tf in self.textFields) {[verCode appendString:tf.text];}//將self和完整輸入的驗證碼傳入delegateif ([self.delegate respondsToSelector:@selector(textFieldsDidEndEditing:verCode:)]) {[self.delegate textFieldsDidEndEditing:self verCode:verCode];}} else {[self.textFields[idx+1] becomeFirstResponder];[[self class] setupBorderColor:textField];[[self class] setupBorderColor:self.textFields[idx+1]];}*stop = YES;}}];}
}#pragma mark - UITextFieldDelegate
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {if (!textField.isFirstResponder) {return NO;}if (string.length) {textField.text = nil;} else {[self.textFields enumerateObjectsUsingBlock:^(YTUnclickableTextField * _Nonnull iTextField, NSUInteger idx, BOOL * _Nonnull stop) {if (iTextField == textField) {if (idx > 0 && [iTextField.text isEqualToString:YTUnclickableTextFieldSpace]) {[self.textFields[idx-1] becomeFirstResponder];[[self class] setupBorderColor:textField];[[self class] setupBorderColor:self.textFields[idx-1]];}//消除錯誤label文字if (self.errorLabel.text) {[self showErrorCodeText:nil];}*stop = YES;}}];}return YES;
}#pragma mark - Private method
+ (void)setupBorderColor:(UITextField *)textField {textField.layer.borderColor = textField.isFirstResponder ? AppStyleColor.CGColor : [UIColor lightGrayColor].CGColor;
}- (void)generateVerCode {self.verifyCodeImageView.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.imageUrlString]]];
}#pragma mark - Public method
- (void)showErrorCodeText:(NSString *)text {self.errorLabel.text = text;
}@end
?? 跳過以上繁雜的布局代碼,只看textField的事件響應(編輯改變監聽不是手動點擊)方法editingChangedWith:和代理方法textField:shouldChangeCharactersInRange: :,設置這兩個方法的目的是分別負責textField焦點的前進后退。
?? 代碼就不多加解釋了,如若感興趣或者有疑問可在下方評論,我會一一作出解答。