验证码输入框

实现效果:


image.png

第一种:
使用多个UITextField来做,底部直接使用UIView画线,这也是最简单的做法,需要处理较多的第一响应事件,如果采用这种方式来做,需要注意的一个坑就是当当前的输入框没有值的时候点击退格键需要删除上一个Field的文字并光标跳转到上一个field,当输入框中没有文字的时候通过UITextField的代理是监听不到退格键点击事件的,需要重写UITextField 的 deleteBackward 方法来监听。
我第一次就是这么实现的,但是后来有个需求是当用户点击任意一个Field的时候光标不能响应在对应的Field上,而是响应在当前输入的最后一位上,只允许用户从最后一位开始删除。这又需要做一些额外的处理,代码量会越来越多,所以我采取了第二种方式。

第二种:
通过一个UITextField 和 一个 UILabel来实现。
UITextField并不显示,只是用来调用起输入键盘以及监听用户的输入。
UILabel通过重新绘制来绘制对应的UI和验证码。

这种方式是我查阅了很多网上的资料后发现的比较合理的一种方式,并进行了一些优化,重点就是绘制的光标添加闪动的动画效果以及控制光标的移动,为了实现这个效果,我放弃了使用上下文的方式来绘制,转而使用的贝塞尔曲线来绘制。

上代码:
YCVerificationCodeInputLabel.h

#import <UIKit/UIKit.h>

@interface YCVerificationCodeInputLabel : UILabel

@property (nonatomic, assign) NSInteger numberOfVertificationCode;

@end

YCVerificationCodeInputLabel.m

#import "YCVerificationCodeInputLabel.h"

@interface YCVerificationCodeInputLabel ()

/** 上一次的光标 */
@property (nonatomic, strong) CAShapeLayer *lastShapeLayer;

@end

@implementation YCVerificationCodeInputLabel

//重写setText方法,当text改变时手动调用drawRect方法,将text的内容按指定的格式绘制到label上
- (void)setText:(NSString *)text {
    [super setText:text];
    // 手动调用drawRect方法
    [self setNeedsDisplay];
}

// 按照指定的格式绘制验证码
- (void)drawRect:(CGRect)rect1 {
    //计算每位验证码的所在区域的宽和高
    CGRect rect = CGRectMake(0, 0, 208, 48);
    float width = (rect.size.width - 16 * (self.numberOfVertificationCode - 1)) / (float)self.numberOfVertificationCode;
    float height = rect.size.height;
    
    // 将每位验证码绘制到指定区域
    for (int i = 0; i < self.text.length; i++) {
        // 计算每位验证码的绘制区域
        CGRect tempRect = CGRectMake(i * (width + 16), 0, width, height);
        // 遍历验证码的每个字符
        NSString *charecterString = [NSString stringWithFormat:@"%c", [self.text characterAtIndex:i]];
        // 设置验证码的现实属性
        NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
        attributes[NSFontAttributeName] = self.font;
        attributes[NSForegroundColorAttributeName] = self.textColor;
        // 计算每位验证码的绘制起点(为了使验证码位于tempRect的中部,不应该从tempRect的重点开始绘制)
        // 计算每位验证码的在指定样式下的size
        CGSize characterSize = [charecterString sizeWithAttributes:attributes];
        CGPoint vertificationCodeDrawStartPoint = CGPointMake(i * (width + 16) + (width - characterSize.width) / 2.0, (tempRect.size.height - characterSize.height) / 2.0);
        // 绘制验证码
        [charecterString drawAtPoint:vertificationCodeDrawStartPoint withAttributes:attributes];
    }
    //绘制底部横线
    for (int k = 0; k < self.numberOfVertificationCode; k++) {
        [self drawBottomLineWithRect:rect andIndex:k];
        [self drawSenterLineWithRect:rect andIndex:k];
    }
}

//绘制底部的线条
- (void)drawBottomLineWithRect:(CGRect)rect1 andIndex:(int)k {
    CGRect rect = rect1;
    float width = (rect.size.width - 16 * (self.numberOfVertificationCode - 1)) / (float)self.numberOfVertificationCode;
    float height = rect.size.height;
    //1.获取上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    //2.设置当前上下问路径
    CGFloat lineHidth = 0.5;
    CGFloat strokHidth = 0.5;
    CGContextSetLineWidth(context, lineHidth);
    CGContextSetStrokeColorWithColor(context,BLUE_2.CGColor);
    CGContextSetFillColorWithColor(context, BLUE_2.CGColor);

    CGRect rectangle = CGRectMake(k * (width + 16), height - strokHidth, width, strokHidth);
    CGContextAddRect(context, rectangle);
    CGContextStrokePath(context);
}

// 绘制中间的输入的线条
- (void)drawSenterLineWithRect:(CGRect)rect1 andIndex:(int)k{
    if (k == self.numberOfVertificationCode - 1
        && self.text.length == self.numberOfVertificationCode) {
        // 最后一位且有字
        [self.lastShapeLayer removeFromSuperlayer];
        self.lastShapeLayer = nil;
        
    } else if (k == self.text.length) {
        // 将光标添加在下一个要输入的输入框上
        [self.lastShapeLayer removeFromSuperlayer];
        self.lastShapeLayer = nil;

        CGRect rect = CGRectMake(0,0,208,48);
        float width = (rect.size.width - 16 * (self.numberOfVertificationCode - 1)) / (float)self.numberOfVertificationCode;
        float height = rect.size.height;
        
        UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(k * (width + 16) + (width - 1.0)/2.0, height/5, 1, height/2)];
        CAShapeLayer *line = [CAShapeLayer layer];
        line.path = path.CGPath;
        line.fillColor = WHITE_1.CGColor;
        line.strokeColor = WHITE_1.CGColor;
        
        [line addAnimation:[self opacityAnimation]
                    forKey:@"kOpacityAnimation"];

        [self.layer addSublayer:line];
        self.lastShapeLayer = line;
    }
}

// 添加光标动画
- (CABasicAnimation *)opacityAnimation {
    CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
    opacityAnimation.fromValue = @(1.0);
    opacityAnimation.toValue = @(0.0);
    opacityAnimation.duration = 0.9;
    opacityAnimation.repeatCount = HUGE_VALF;
    opacityAnimation.removedOnCompletion = YES;
    opacityAnimation.fillMode = kCAFillModeForwards;
    opacityAnimation.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
    return opacityAnimation;
}

@end

YCVerificationCodeInputView

#import <UIKit/UIKit.h>

@interface YCVerificationCodeInputView : UIView

/**
 验证码的位数
 */
@property (nonatomic, assign) NSInteger numberOfVertificationCode;

/**
 验证码
 */
@property (nonatomic, copy) NSString *vertificationCode;

- (void)becomeFirstResponder;

@end

YCVerificationCodeInputView

#import "YCVerificationCodeInputView.h"

#pragma mark - view
#import "YCVerificationCodeInputLabel.h"

@interface YCVerificationCodeInputView () <UITextFieldDelegate>

/**用于获取键盘输入的内容,实际不显示*/
@property (nonatomic, strong) UITextField *textField;
/**实际用于显示验证码/密码的label*/
@property (nonatomic, strong) YCVerificationCodeInputLabel *label;

@end

@implementation YCVerificationCodeInputView

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        // 设置透明背景色
        self.backgroundColor = [UIColor clearColor];
        // 设置验证码的位数默认为四位
        self.numberOfVertificationCode = 4;
        /* 调出键盘的textField */
        self.textField = [[UITextField alloc] initWithFrame:self.bounds];
        // 隐藏textField,通过点击IDVertificationCodeInputView使其成为第一响应者,来弹出键盘
        self.textField.hidden = YES;
        self.textField.keyboardType = UIKeyboardTypeNumberPad;
        self.textField.delegate = self;
        // 将textField放到最后边
        [self insertSubview:self.textField atIndex:0];
        
        /* 添加用于显示验证码的label */
        self.label = [[YCVerificationCodeInputLabel alloc] initWithFrame:self.bounds];
        self.label.numberOfVertificationCode = self.numberOfVertificationCode;
        self.label.font = [UIFont fontWithName:BOLD_FONT size:24];
        self.label.textColor = WHITE_1;
        
        [self addSubview:self.label];
    }
    return self;
}

- (void)setNumberOfVertificationCode:(NSInteger)numberOfVertificationCode {
    _numberOfVertificationCode = numberOfVertificationCode;
    // 保持label的验证码/密码位数与IDVertificationCodeInputView一致,此时label一定已经被创建
    self.label.numberOfVertificationCode = numberOfVertificationCode;
}

- (void)becomeFirstResponder{
    [self.textField becomeFirstResponder];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.textField becomeFirstResponder];
}

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
    // 判断是不是“删除”字符
    if (string.length != 0) { //不是“删除”字符
        // 判断验证码/密码的位数是否达到预定的位数
        if (textField.text.length < self.numberOfVertificationCode) {
            self.label.text = [textField.text stringByAppendingString:string];
            self.vertificationCode = self.label.text;
            if (self.label.text.length == self.numberOfVertificationCode) {
                NSLog(@"tag 已经输入完成验证码了vertificationCode= %@",_vertificationCode);
            }
            return YES;
        } else {
            return NO;
        }
    } else { //是“删除”字符
        self.label.text = [textField.text substringToIndex:textField.text.length -1];
        self.vertificationCode = self.label.text;
        return YES;
    }
}
@end

具体使用姿势:
YCEntranceVerificationVC

- (void)viewDidLoad {
    [super viewDidLoad];
    xxx...
    [self configUI];
    [self bindingData];

    [self.inputView becomeFirstResponder];
}

- (void)configUI {
    xxx...    
    _inputView = [[YCVerificationCodeInputView alloc] initWithFrame:CGRectMake(self.phoneNumberLabel.yc_centerX, self.phoneNumberLabel.yc_bottom + 24, 208, 48)];
    [self.view addSubview:self.inputView];
    
    xxx...    
}

- (void)bindingData {
    @weakify(self);
    // 监听完整的验证码
    RAC(self.viewModel, captcha) = RACObserve(self, currentCaptcha);
    
    // 监听验证码的输入,输入完成自动发送验证
    [[RACObserve(self.inputView, vertificationCode) map:^id(id value) {
        @strongify(self);
        NSString *vertificationCode = value;
        if (vertificationCode.length == 4) {
            self.currentCaptcha = vertificationCode;
            return @YES;
        } else {
            return @NO;
        }
        
    }] subscribeNext:^(NSNumber *x) {
        @strongify(self);
        if (x.boolValue) {
      
            // 请求服务器验证验证码
    xxx...
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容