iOS指纹解锁和手势解锁
[TOC]
前言
一直想写博客来着,一来可以记录一些自己学习和研究的东西,二来也可以将自己写的一些东西分享出去,给他人参考,还可能收到他人的一些建议,从而完善自己的项目和提升自己的技术,这也是一种很好的技术交流方式。但是之前一直不知道怎么去写?怎么去总结?在经过一些观摩和学习后,终于决定先来试试水了😂。下面正式开始我的第一篇博客。
这篇博客是自己基于iOS
系统实现的指纹解锁(系统API)和手势解锁(CAShapeLayer)功能。
在之前自学CAAnimation
,再加上公司老大说可以预研(之前没有做过)一下各种解锁方式的情况下,想着自己来实现一下现在常用的解锁方式:指纹解锁
和手势解锁
。
指纹解锁
基于iOS的指纹解锁其实很简单,因为系统已经提供了API
给你,你只需要做一些简单的判断和适时的调用就可以了。
第一步
首先导入头文件#import <LocalAuthentication/LocalAuthentication.h>
判断是否开启了TouchID,如果已经开启,直接校验指纹,如果未开启,则需要先开启TouchID
1 2
| [[[NSUserDefaults standardUserDefaults] objectForKey:@"OpenTouchID"] boolValue]
|
第二步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| - (void)p_openTouchID { dispatch_async(dispatch_get_main_queue(), ^{ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"是否开启TouchID?" preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"YES" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { [[NSUserDefaults standardUserDefaults] setObject:@(YES) forKey:@"OpenTouchID"]; [[NSNotificationCenter defaultCenter] postNotificationName:@"OpenTouchIDSuccess" object:nil userInfo:nil]; }]]; [alertController addAction:[UIAlertAction actionWithTitle:@"NO" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { [[NSUserDefaults standardUserDefaults] setObject:@(NO) forKey:@"OpenTouchID"]; [[NSNotificationCenter defaultCenter] postNotificationName:@"OpenTouchIDSuccess" object:nil userInfo:nil]; }]]; [self presentViewController:alertController animated:YES completion:nil]; }); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| - (void)p_touchID { dispatch_async(dispatch_get_main_queue(), ^{ LAContext *context = [[LAContext alloc] init]; NSError *error = nil; if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) { [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"TouchID Text" reply:^(BOOL success, NSError * _Nullable error) { if (success) { [[NSNotificationCenter defaultCenter] postNotificationName:@"UnlockLoginSuccess" object:nil]; }else { switch (error.code) { case LAErrorAuthenticationFailed: { NSLog(@"授权失败"); [[NSNotificationCenter defaultCenter] postNotificationName:@"touchIDFailed" object:nil]; } break; case LAErrorUserCancel: { NSLog(@"用户取消验证Touch ID"); [self dismissViewControllerAnimated:YES completion:nil]; } break; case LAErrorUserFallback: { [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [[NSNotificationCenter defaultCenter] postNotificationName:@"touchIDFailed" object:nil]; NSLog(@"用户选择输入密码,切换主线程处理"); }]; } break; case LAErrorSystemCancel: { NSLog(@"取消授权,如其他应用切入,用户自主"); } break; case LAErrorPasscodeNotSet: { NSLog(@"设备系统未设置密码"); } break; case LAErrorBiometryNotAvailable: { NSLog(@"设备未设置Touch ID"); } break; case LAErrorBiometryNotEnrolled: { NSLog(@"用户未录入指纹"); } break; #if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0 case LAErrorBiometryLockout: { NSLog(@"Touch ID被锁,需要用户输入密码解锁"); } break; case LAErrorAppCancel: { NSLog(@"用户不能控制情况下APP被挂起"); } break; case LAErrorInvalidContext: { NSLog(@"LAContext传递给这个调用之前已经失效"); } break; #else #endif default: { [[NSOperationQueue mainQueue] addOperationWithBlock:^{ NSLog(@"其他情况,切换主线程处理"); }]; break; } } } }]; }else { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"该设备不支持TouchID" preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"完成" style:UIAlertActionStyleCancel handler:nil]]; [self presentViewController:alertController animated:YES completion:nil]; } }); }
|
注:代码中的NSNotificationCenter
用于不同操作后的界面跳转,重新设置window.rootViewController
,可忽略。
到这里指纹解锁
就结束了,很简单的一个API
调用。
手势解锁
其实在之前还没有接触和刚开始接触iOS开发的时候,觉得手势解锁很难,完全不知道怎么去实现?但是当我在自学CAAnimation的时候,脑海中突然就想到了一个实现手势解锁的方案,下面就开始介绍我的实现方法:
构思
手势解锁是怎么去验证你滑动的手势是正确的?
其实手势解锁和输入密码的验证是一样的,在你画UI的时候,你可以给每一个*圆点*一个id,在你设置手势的时候,将滑动到对应*圆点*的id放入一个有序集合中,并保存起来,然后验证登录的时候,用另外一个有序集合记录你当前滑动到的*圆点*id,然后和之前保存在本地的进行对比,就可以达到验证的目的了
用什么方式去具体实现UI?
在之前想过几种实现方式,但是都被pass掉了,直到自学CAAnimation的时候,才突然意识到有一个很好的实现方式----CAShapeLayer
其实,当你有了这两个问题的答案的时候,你的手势解锁就已经实现了一大部分,后面的部分就是敲代码了。
实现 (工程代码见文末链接)
先上几张效果图:(由于本人艺术细胞有限,所以为了好看点,界面的UI是参照QQ安全中心的手势解锁)
目录结构
GesturesViewController
:这个controller
用于展示UI
,你可以替换成自己controller
,
GesturesView
:用于圆点
按钮的初始化和布局,
PointView
:圆点
手势按钮。
这里主要介绍一下GesturesView和PointView,主要逻辑也都在这两个类中:
PointView(主要是界面UI,不多介绍,直接上代码)
PointView.h
1 2 3 4 5 6 7 8 9 10 11
| - (instancetype)initWithFrame:(CGRect)frame withID:(NSString *)ID;
@property (nonatomic, copy, readonly) NSString *ID;
@property (nonatomic, assign) BOOL isSelected;
@property (nonatomic, assign) BOOL isError;
@property (nonatomic, assign) BOOL isSuccess;
|
-initWithFrame:withID:
传入frame
和ID
,用于初始化PointView
,
ID:
只读,用于外部获取ID
,
isSelected,isError,isSuccess:
用于判断PointView的状态以显示不同的UI。
PointView.m
通过懒加载初始化三个CAShapeLayer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| #pragma mark - 懒加载
- (CAShapeLayer *)contentLayer { if (!_contentLayer) { _contentLayer = [CAShapeLayer layer]; UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(2.0, 2.0, SELF_WIDTH - 4.0, SELF_HEIGHT - 4.0) cornerRadius:(SELF_WIDTH - 4.0) / 2.0]; _contentLayer.path = path.CGPath; _contentLayer.fillColor = RGBCOLOR(46.0, 47.0, 50.0).CGColor; _contentLayer.strokeColor = RGBCOLOR(26.0, 27.0, 29.0).CGColor; _contentLayer.strokeStart = 0; _contentLayer.strokeEnd = 1; _contentLayer.lineWidth = 2; _contentLayer.cornerRadius = self.bounds.size.width / 2.0; } return _contentLayer; }
- (CAShapeLayer *)borderLayer { if (!_borderLayer) { _borderLayer = [CAShapeLayer layer]; UIBezierPath *borderPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(SELF_WIDTH / 2.0, SELF_HEIGHT / 2.0) radius:SELF_WIDTH / 2.0 startAngle:0 endAngle:2 * M_PI clockwise:NO]; _borderLayer.strokeColor = RGBCOLOR(105.0, 108.0, 111.0).CGColor; _borderLayer.fillColor = [UIColor clearColor].CGColor; _borderLayer.strokeEnd = 1; _borderLayer.strokeStart = 0; _borderLayer.lineWidth = 2; _borderLayer.path = borderPath.CGPath; } return _borderLayer; }
- (CAShapeLayer *)centerLayer { if (!_centerLayer) { _centerLayer = [CAShapeLayer layer]; UIBezierPath *centerPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(SELF_WIDTH / 2.0 - (SELF_WIDTH - 4.0) / 4.0, SELF_HEIGHT / 2.0 - (SELF_HEIGHT - 4.0) / 4.0, (SELF_WIDTH - 4.0) / 2.0, (SELF_WIDTH - 4.0) / 2.0) cornerRadius:(SELF_WIDTH - 4.0) / 4.0]; _centerLayer.path = centerPath.CGPath; _centerLayer.lineWidth = 3; _centerLayer.strokeColor = [UIColor colorWithWhite:0 alpha:0.7].CGColor; _centerLayer.fillColor = RGBCOLOR(30.0, 180.0, 244.0).CGColor; } return _centerLayer; }
|
设置PointView的UI状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| - (void)setIsSuccess:(BOOL)isSuccess { _isSuccess = isSuccess; if (_isSuccess) { self.centerLayer.fillColor = RGBCOLOR(43.0, 210.0, 110.0).CGColor; }else { self.centerLayer.fillColor = RGBCOLOR(30.0, 180.0, 244.0).CGColor; } }
- (void)setIsSelected:(BOOL)isSelected { _isSelected = isSelected; if (_isSelected) { self.centerLayer.hidden = NO; self.borderLayer.strokeColor = RGBCOLOR(30.0, 180.0, 244.0).CGColor; }else { self.centerLayer.hidden = YES; self.borderLayer.strokeColor = RGBCOLOR(105.0, 108.0, 111.0).CGColor; } }
- (void)setIsError:(BOOL)isError { _isError = isError; if (_isError) { self.centerLayer.fillColor = RGBCOLOR(222.0, 64.0, 60.0).CGColor; }else { self.centerLayer.fillColor = RGBCOLOR(30.0, 180.0, 244.0).CGColor; } }
|
GesturesView(基本所有的逻辑都在这个里面了)
GesturesView.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| typedef void (^GestureBlock)(NSArray *selectedID);
typedef void (^UnlockBlock)(BOOL isSuccess);
typedef void (^SettingBlock)(void);
@interface GesturesView : UIView
@property (nonatomic, copy) GestureBlock gestureBlock;
@property (nonatomic, copy) UnlockBlock unlockBlock;
@property (nonatomic, copy) SettingBlock settingBlock;
@property (nonatomic, assign) BOOL settingGesture;
|
这里我申明了三个block:
GestureBlock:
将选择的ID有序集合回传给控制器,
UnlockBlock:
回传手势验证结果,
SettingBlcok:
设置手势失败
属性:
gestureBlock,unlockBlock,settingBlock:
分别是对应block的实例,
settingGesture:
用于判断是设置手势还是手势解锁
GesturesView.h (最主要的逻辑实现部分)
私有属性部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @property (nonatomic, strong) NSMutableArray *pointViews;
@property (nonatomic, assign) CGPoint startPoint;
@property (nonatomic, assign) CGPoint endPoint;
@property (nonatomic, strong) NSMutableArray *selectedView;
@property (nonatomic, strong) CAShapeLayer *lineLayer;
@property (nonatomic, strong) UIBezierPath *linePath;
@property (nonatomic, strong) NSMutableArray *selectedViewCenter;
@property (nonatomic, assign) BOOL touchEnd;
|
代码实现部分:
初始化startPoint
,endPoint
以及9个PointView
按钮,startPoint
和endPoint
默认为0
,并设置PointView
的ID
:
1 2 3 4 5 6 7 8 9 10
| self.startPoint = CGPointZero; self.endPoint = CGPointZero; for (int i = 0; i<9 ; i++) { PointView *pointView = [[PointView alloc] initWithFrame:CGRectMake((i % 3) * (SELF_WIDTH / 2.0 - 31.0) + 1, (i / 3) * (SELF_HEIGHT / 2.0 - 31.0) + 1, 60, 60) withID:[NSString stringWithFormat:@"gestures %d",i + 1]]; [self addSubview:pointView]; [self.pointViews addObject:pointView]; }
|
滑动事件:
如果self.touchEnd
为YES
则直接return,为NO
开始以下处理:
- 首先获取到滑动的点,遍历所有的
PointView
,判断该点是否在某个手势按钮
范围,在范围内记录状态,否则不做处理;
- 判断
self.startPoint
是否为CGPointZero
,如果为YES
,则将该手势按钮
的center
赋值给self.startPoint
;
- 判断该手势按钮的
center
是否包含在self.selectedViewCenter
中,如果为YES
,忽略此次记录,为NO
则记录该中心点,用于画线
,同样记录该手势按钮
的ID
,用于记录保存手势密码;
- 如果
self.startPoint
不为CGPointZero
,则记录当前滑动到的点为self.endPoint
,并划线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { if (self.touchEnd) { return; } UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self]; for (PointView *pointView in self.pointViews) { if (CGRectContainsPoint(pointView.frame, point)) { if (CGPointEqualToPoint(self.startPoint, CGPointZero)) { self.startPoint = pointView.center; } if (![self.selectedViewCenter containsObject:[NSValue valueWithCGPoint:pointView.center]]) { [self.selectedViewCenter addObject:[NSValue valueWithCGPoint:pointView.center]]; } if (![self.selectedView containsObject:pointView.ID]) { [self.selectedView addObject:pointView.ID]; pointView.isSelected = YES; } } } if (!CGPointEqualToPoint(self.startPoint, CGPointZero)) { self.endPoint = point; [self p_drawLines]; } }
|
如果self.touchEnd
为YES
则直接return,为NO
开始画线:
- 首先移除
self.lineLayer
,self.linePath
,否则你会发现随着你的滑动,会出现很多条线。;
- 设置
self.linePath
的起始点,并遍历self.selectedViewCenter
,为self.linePath
添加节点,最后将self.endPoint
添加上去(为结束滑动的时候,self.endPoint
为当前滑动位置的点);
- 设置
self.lineLayer
的相应属性,并添加到self.layer
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| - (void)p_drawLines { if (self.touchEnd) { return; } [self.lineLayer removeFromSuperlayer]; [self.linePath removeAllPoints]; [self.linePath moveToPoint:self.startPoint]; for (NSValue *pointValue in self.selectedViewCenter) { [self.linePath addLineToPoint:[pointValue CGPointValue]]; } [self.linePath addLineToPoint:self.endPoint]; self.lineLayer.path = self.linePath.CGPath; self.lineLayer.lineWidth = 4.0; self.lineLayer.strokeColor = RGBCOLOR(30.0, 180.0, 244.0).CGColor; self.lineLayer.fillColor = [UIColor clearColor].CGColor; [self.layer addSublayer:self.lineLayer]; self.layer.masksToBounds = YES; }
|
- 将
self.endPoint
设置为self.selectedViewCenter.lastObject
,如果self.endPoint
还是为CGPointZero
,则说明未滑动到手势按钮
范围,不做任何处理,否则继续以下逻辑处理;
- 再次调用
-(void)p_drawLines
画线;
- 判断是
设置手势密码
还是手势解锁
;
设置手势密码
:
- 如果选中的
手势按钮
数量少于4,设置self.touchEnd = NO
使其可以重新设置,return
结束此次设置;
- 如果设置的
手势按钮
符合要求则调用self.gestureBlock(self.selectedView)
将手势密码回传给控制器
;
手势解锁
:
- 获取本地存储的
手势密码
;我这里用的是NSUserDefaults
,其实这是不安全的,建议使用Keychain
,我也会在后续的更新中使用Keychain
已使用keychain保存密码,具体使用见Demo
- 如果
self.selectedView
和本地手势密码一样,则解锁成功,并设置pointView.isSuccess = YES
改变手势按钮
样式等,并调用self.unlockBlock(YES)
,告知控制器
结果;
- 否则解锁失败,
pointView.isError = YES
改变手势按钮
样式等,并调用self.unlockBlock(NO)
,告知控制器
结果;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { self.endPoint = [self.selectedViewCenter.lastObject CGPointValue]; if (CGPointEqualToPoint(self.endPoint, CGPointZero)) { return; } [self p_drawLines]; self.touchEnd = YES; if (_gestureBlock && _settingGesture) { if (self.selectedView.count < 4) { self.touchEnd = NO; for (PointView *pointView in self.pointViews) { pointView.isSelected = NO; } [self.lineLayer removeFromSuperlayer]; [self.selectedView removeAllObjects]; self.startPoint = CGPointZero; self.endPoint = CGPointZero; [self.selectedViewCenter removeAllObjects]; if (_settingBlock) { self.settingBlock(); } return; } _gestureBlock(self.selectedView); return; } NSArray *selectedID = [[NSUserDefaults standardUserDefaults] objectForKey:@"GestureUnlock"]; if ([self.selectedView isEqualToArray:selectedID]) { for (PointView *pointView in self.pointViews) { pointView.isSuccess = YES; } self.lineLayer.strokeColor = RGBCOLOR(43.0, 210.0, 110.0).CGColor; if (_unlockBlock) { self.unlockBlock(YES); } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:@"UnlockLoginSuccess" object:nil]; }); }else { for (PointView *pointView in self.pointViews) { pointView.isError = YES; } self.lineLayer.strokeColor = RGBCOLOR(222.0, 64.0, 60.0).CGColor; if (_unlockBlock) { self.unlockBlock(NO); } } }
|
到这里就实现了手势解锁
的所有逻辑,在实现之前还在担心有什么问题,结果实现出来之后感觉其实很简单。
最后
希望这篇文章能够帮助到一些人。对于代码
和博客
的一些规范
希望大家谅解一下了,后面也会慢慢去优化的。最后的最后附上Demo的链接 Demo-GitHub。