iOS指紋解鎖和手勢解鎖

前言

一直想寫博客來着,一來能夠記錄一些本身學習和研究的東西,二來也能夠將本身寫的一些東西分享出去,給他人蔘考,還可能收到他人的一些建議,從而完善本身的項目和提高本身的技術,這也是一種很好的技術交流方式。可是以前一直不知道怎麼去寫?怎麼去總結?在通過一些觀摩和學習後,終於決定先來試試水了😂。下面正式開始個人第一篇博客。git

這篇博客是本身基於iOS系統實現的指紋解鎖(系統API)和手勢解鎖(CAShapeLayer)功能。github

在以前自學CAAnimation,再加上公司老大說能夠預研(以前沒有作過)一下各類解鎖方式的狀況下,想着本身來實現一下如今經常使用的解鎖方式:指紋解鎖手勢解鎖數組

指紋解鎖

基於iOS的指紋解鎖其實很簡單,由於系統已經提供了API給你,你只須要作一些簡單的判斷和適時的調用就能夠了。安全

第一步

首先導入頭文件#import <LocalAuthentication/LocalAuthentication.h>async

判斷是否開啓了TouchID,若是已經開啓,直接校驗指紋,若是未開啓,則須要先開啓TouchID佈局

//判斷是否開啓了TouchID
[[[NSUserDefaults standardUserDefaults] objectForKey:@"OpenTouchID"] boolValue]
複製代碼

第二步

  • 未開啓TouchID,詢問是否開啓
- (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) {
            //開啓TouchID
            [[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) {
            //不開啓TouchID
            [[NSUserDefaults standardUserDefaults] setObject:@(NO) forKey:@"OpenTouchID"];
            [[NSNotificationCenter defaultCenter] postNotificationName:@"OpenTouchIDSuccess" object:nil userInfo:nil];
        }]];
        [self presentViewController:alertController animated:YES completion:nil];
    });
}
複製代碼
  • 已開啓TouchID
- (void)p_touchID
{
    dispatch_async(dispatch_get_main_queue(), ^{
        LAContext *context = [[LAContext alloc] init];
        NSError *error = nil;
        //判斷是否支持TouchID
        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(@"受權失敗"); // -1 連續三次指紋識別錯誤
                            [[NSNotificationCenter defaultCenter] postNotificationName:@"touchIDFailed" object:nil];
                        }
                            break;
                        case LAErrorUserCancel:
                        {
                            NSLog(@"用戶取消驗證Touch ID"); // -2 在TouchID對話框中點擊了取消按鈕
                            [self dismissViewControllerAnimated:YES completion:nil];
                        }
                            break;
                        case LAErrorUserFallback:
                        {
                            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                                [[NSNotificationCenter defaultCenter] postNotificationName:@"touchIDFailed" object:nil];
                                NSLog(@"用戶選擇輸入密碼,切換主線程處理"); // -3 在TouchID對話框中點擊了輸入密碼按鈕
                            }];
                            
                        }
                            break;
                        case LAErrorSystemCancel:
                        {
                            NSLog(@"取消受權,如其餘應用切入,用戶自主"); // -4 TouchID對話框被系統取消,例如按下Home或者電源鍵
                        }
                            break;
                        case LAErrorPasscodeNotSet:
                            
                        {
                            NSLog(@"設備系統未設置密碼"); // -5
                        }
                            break;
                        case LAErrorBiometryNotAvailable:
                        {
                            NSLog(@"設備未設置Touch ID"); // -6
                        }
                            break;
                        case LAErrorBiometryNotEnrolled: // Authentication could not start, because Touch ID has no enrolled fingers
                        {
                            NSLog(@"用戶未錄入指紋"); // -7
                        }
                            break;
                            
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0
                        case LAErrorBiometryLockout: //Authentication was not successful, because there were too many failed Touch ID attempts and Touch ID is now locked. Passcode is required to unlock Touch ID, e.g. evaluating LAPolicyDeviceOwnerAuthenticationWithBiometrics will ask for passcode as a prerequisite 用戶連續屢次進行Touch ID驗證失敗,Touch ID被鎖,須要用戶輸入密碼解鎖,先Touch ID驗證密碼
                        {
                            NSLog(@"Touch ID被鎖,須要用戶輸入密碼解鎖"); // -8 連續五次指紋識別錯誤,TouchID功能被鎖定,下一次須要輸入系統密碼
                        }
                            break;
                        case LAErrorAppCancel:
                        {
                            NSLog(@"用戶不能控制狀況下APP被掛起"); // -9
                        }
                            break;
                        case LAErrorInvalidContext:
                        {
                            NSLog(@"LAContext傳遞給這個調用以前已經失效"); // -10
                        }
                            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,可忽略。post

到這裏指紋解鎖就結束了,很簡單的一個API調用。學習


手勢解鎖

其實在以前尚未接觸和剛開始接觸iOS開發的時候,以爲手勢解鎖很難,徹底不知道怎麼去實現?可是當我在自學CAAnimation的時候,腦海中忽然就想到了一個實現手勢解鎖的方案,下面就開始介紹個人實現方法:優化

構思

  1. 手勢解鎖是怎麼去驗證你滑動的手勢是正確的?ui

    其實手勢解鎖和輸入密碼的驗證是同樣的,在你畫UI的時候,你能夠給每個*圓點*一個id,在你設置手勢的時候,將滑動到對應*圓點*的id放入一個有序集合中,並保存起來,而後驗證登陸的時候,用另一個有序集合記錄你當前滑動到的*圓點*id,而後和以前保存在本地的進行對比,就能夠達到驗證的目的了

  2. 用什麼方式去具體實現UI?

    在以前想過幾種實現方式,可是都被pass掉了,直到自學CAAnimation的時候,才忽然意識到有一個很好的實現方式----CAShapeLayer

其實,當你有了這兩個問題的答案的時候,你的手勢解鎖就已經實現了一大部分,後面的部分就是敲代碼了。

實現 (工程代碼見文末連接)

先上幾張效果圖:(因爲本人藝術細胞有限,因此爲了好看點,界面的UI是參照QQ安全中心的手勢解鎖)

手勢解鎖

目錄結構

手勢解鎖目錄結構

  • GesturesViewController:這個controller用於展現UI,你能夠替換成本身controller,
  • GesturesView:用於圓點按鈕的初始化和佈局,
  • PointView圓點手勢按鈕。

這裏主要介紹一下GesturesView和PointView,主要邏輯也都在這兩個類中:

PointView(主要是界面UI,很少介紹,直接上代碼)

PointView.h

- (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:傳入frameheID,用於初始化PointView

  • ID:只讀,用於外部獲取ID

  • isSelected,isError,isSuccess:用於判斷PointView的狀態以顯示不通的UI。

PointView.m

經過懶加載初始化三個CAShapeLayer

#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狀態

//根據狀況顯示三種狀態
- (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

//回傳選擇的id
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 (最主要的邏輯實現部分)

私有屬性部分:

//可變數組,用於存放初始化的點擊按鈕
@property (nonatomic, strong) NSMutableArray             *pointViews;
//記錄手勢滑動的起始點
@property (nonatomic, assign) CGPoint                    startPoint;
//記錄手勢滑動的結束點
@property (nonatomic, assign) CGPoint                    endPoint;
//存儲選中的按鈕ID
@property (nonatomic, strong) NSMutableArray             *selectedView;
//手勢滑動通過的點的連線
@property (nonatomic, strong) CAShapeLayer               *lineLayer;
//手勢滑動的path
@property (nonatomic, strong) UIBezierPath               *linePath;
//用於存儲選中的按鈕
@property (nonatomic, strong) NSMutableArray             *selectedViewCenter;
//判斷時候滑動是否結束
@property (nonatomic, assign) BOOL                       touchEnd;
複製代碼

代碼實現部分:

初始化startPointendPoint以及9PointView按鈕,startPointendPoint默認爲0,並設置PointViewID

//初始化開始點位和結束點位
    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.touchEndYES則直接return,爲NO開始如下處理:

  1. 首先獲取到滑動的點,遍歷全部的PointView,判斷該點是否在某個手勢按鈕範圍,在範圍內記錄狀態,不然不作處理;
  2. 判斷self.startPoint是否爲CGPointZero,若是爲YES,則將該手勢按鈕center賦值給self.startPoint;
  3. 判斷該手勢按鈕的center是否包含在self.selectedViewCenter中,若是爲YES,忽略這次記錄,爲NO則記錄該中心點,用於畫線,一樣記錄該手勢按鈕ID,用於記錄保存手勢密碼;
  4. 若是self.startPoint不爲CGPointZero,則記錄當前滑動到的點爲self.endPoint,並劃線。
//touch事件
- (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)) {
            //若是開始按鈕爲zero,記錄開始按鈕,不然不須要記錄開始按鈕
            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;
            }
        }
    }
    //若是開始點位不爲zero則記錄結束點位,不然跳過不記錄
    if (!CGPointEqualToPoint(self.startPoint, CGPointZero)) {
        self.endPoint = point;
        [self p_drawLines];
    }
}
複製代碼
  • 畫線:

若是self.touchEndYES則直接return,爲NO開始畫線:

  1. 首先移除self.lineLayer,self.linePath,不然你會發現隨着你的滑動,會出現不少條線。
  2. 設置self.linePath的起始點,並遍歷self.selectedViewCenter,爲self.linePath添加節點,最後將self.endPoint添加上去(爲結束滑動的時候,self.endPoint爲當前滑動位置的點);
  3. 設置self.lineLayer的相應屬性,並添加到self.layer
//畫線
- (void)p_drawLines
{
    //結束手勢滑動,不畫線
    if (self.touchEnd) {
        return;
    }
    //移除path的點和lineLayer
    [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;
}
複製代碼
  • 結束滑動:
  1. self.endPoint設置爲self.selectedViewCenter.lastObject,若是self.endPoint仍是爲CGPointZero,則說明未滑動到手勢按鈕範圍,不作任何處理,不然繼續如下邏輯處理;
  2. 再次調用-(void)p_drawLines畫線;
  3. 判斷是設置手勢密碼仍是手勢解鎖
    1. 設置手勢密碼
      1. 若是選中的手勢按鈕數量少於4,設置self.touchEnd = NO使其能夠從新設置,return結束這次設置;
      2. 若是設置的手勢按鈕符合要求則調用self.gestureBlock(self.selectedView)將手勢密碼回傳給控制器
    2. 手勢解鎖
      1. 獲取本地存儲的手勢密碼我這裏用的是NSUserDefaults,其實這是不安全的,建議使用Keychain,我也會在後續的更新中使用Keychain 已使用keychain保存密碼,具體使用見Demo
      2. 若是self.selectedView和本地手勢密碼同樣,則解鎖成功,並設置pointView.isSuccess = YES改變手勢按鈕樣式等,並調用self.unlockBlock(YES),告知控制器結果;
      3. 不然解鎖失敗,pointView.isError = YES改變手勢按鈕樣式等,並調用self.unlockBlock(NO),告知控制器結果;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //結束手勢滑動的時候,將結束按鈕設置爲最後一個手勢按鈕的中心點,並畫線
    self.endPoint = [self.selectedViewCenter.lastObject CGPointValue];
    //若是endPoint仍是爲zero說明未滑動到有效位置,不作處理
    if (CGPointEqualToPoint(self.endPoint, CGPointZero)) {
        return;
    }
    [self p_drawLines];
    //改變手勢滑動結束的狀態,爲yes則沒法在滑動手勢劃線
    self.touchEnd = YES;
    //設置手勢時,返回設置的時候密碼,不然繼續下面的操做進行手勢解鎖
    if (_gestureBlock && _settingGesture) {
        //手勢密碼不得小於4個點
        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]) {
        //解鎖成功,遍歷pointview,設置爲成功狀態
        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 {//解鎖失敗
        //解鎖失敗,遍歷pointView,設置爲失敗狀態
        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

相關文章
相關標籤/搜索