iOS指紋解鎖和手勢解鎖

yinxing29發表於2018-03-14

前言

一直想寫部落格來著,一來可以記錄一些自己學習和研究的東西,二來也可以將自己寫的一些東西分享出去,給他人蔘考,還可能收到他人的一些建議,從而完善自己的專案和提升自己的技術,這也是一種很好的技術交流方式。但是之前一直不知道怎麼去寫?怎麼去總結?在經過一些觀摩和學習後,終於決定先來試試水了?。下面正式開始我的第一篇部落格。

這篇部落格是自己基於iOS系統實現的指紋解鎖(系統API)和手勢解鎖(CAShapeLayer)功能。

在之前自學CAAnimation,再加上公司老大說可以預研(之前沒有做過)一下各種解鎖方式的情況下,想著自己來實現一下現在常用的解鎖方式:指紋解鎖手勢解鎖

指紋解鎖

基於iOS的指紋解鎖其實很簡單,因為系統已經提供了API給你,你只需要做一些簡單的判斷和適時的呼叫就可以了。

第一步

首先匯入標頭檔案#import <LocalAuthentication/LocalAuthentication.h>

判斷是否開啟了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,可忽略。

到這裡指紋解鎖就結束了,很簡單的一個API呼叫。


手勢解鎖

其實在之前還沒有接觸和剛開始接觸iOS開發的時候,覺得手勢解鎖很難,完全不知道怎麼去實現?但是當我在自學CAAnimation的時候,腦海中突然就想到了一個實現手勢解鎖的方案,下面就開始介紹我的實現方法:

構思

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

    其實手勢解鎖和輸入密碼的驗證是一樣的,在你畫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

相關文章