OC学习55:FaceID & TouchID & Guesture

张建 lol

前言

随着 iPhone 系统的升级,iOS 的登录方式也随之增多,其中指纹、脸部、手势登录,越来越常见,下面为大家介绍其登录的方式

FaceID

iPhoneX 以及上机型,人脸解锁 俗称 FaceID,并且需要 iOS 11.0及以上,因此需要满足两个条件才能调用系统的 FaceID

  • iPhoneX及以上机型
  • iOS 11.0 及以上系统

faceID 和其他的面部识别,虹膜识别不同,解锁用的是 红外线投影,然后 另一个红外线接收器捕捉信号,黑暗中也能解锁。

TouchID

iPhone 5siPhone 8 系列,支持 指纹识别TouchID,不支持人脸识别,同样的需要满足两个条件才能使用 TouchID:

  • iPhone 5s 到 iPhone 8机型
  • iOS 8.0及以上系统

FaceID & TouchID

由于 FaceIDTouchID 用的同一个框架,因此需要放在一起实现。

  1. 引入框架
1
#import <LocalAuthentication/LocalAuthentication.h>
  1. LAPolicy

它是一个枚举,根据自己的需要选择 LAPolicy,它提供了两个值:

  • LAPolicyDeviceOwnerAuthenticationWithBiometrics 支持iOS8.0
1
2
3
4
5
LAPolicyDeviceOwnerAuthenticationWithBiometrics API_AVAILABLE(ios(8.0), macos(10.12.2))

Device owner will be authenticated using a biometric method (Touch ID or Face ID).

// 设备所有者将使用生物识别方法(Touch ID或Face ID)进行身份验证。
  • LAPolicyDeviceOwnerAuthentication 支持iOS9.0
1
2
3
4
5
LAPolicyDeviceOwnerAuthentication API_AVAILABLE(ios(9.0), macos(10.11), watchos(3.0))

Device owner will be authenticated by biometry or device passcode.

// 设备所有者将通过生物识别或设备密码进行身份验证。
  1. canEvaluatePolicy

使用 canEvaluatePolicy 方法判断设备是否支持 生物识别,返回 YES 支持,返回 NO 不支持。

1
2
3
4
5
6
// 初始化错误对象指针
NSError * error = nil;
// 判断是否支持生物识别
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {

}
  1. LAError
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
// 身份验证不成功,因为用户无法提供有效的凭据。
LAErrorAuthenticationFailed = kLAErrorAuthenticationFailed,

// 认证被用户取消(例如了取消按钮)。
LAErrorUserCancel = kLAErrorUserCancel,

// 认证被取消了,因为用户利用回退按钮(输入密码)。
LAErrorUserFallback = kLAErrorUserFallback,

// 身份验证被系统取消了(如另一个应用程序去前台)。
LAErrorSystemCancel = kLAErrorSystemCancel,

// 身份验证无法启动,因为设备没有设置密码。
LAErrorPasscodeNotSet = kLAErrorPasscodeNotSet,

// 身份验证无法启动,因为Touch ID不可用在设备上。
LAErrorTouchIDNotAvailable NS_ENUM_DEPRECATED(10_10, 10_13, 8_0, 11_0, "use LAErrorBiometryNotAvailable") = kLAErrorTouchIDNotAvailable,

// 身份验证无法启动,因为没有登记的Touch ID。
LAErrorTouchIDNotEnrolled NS_ENUM_DEPRECATED(10_10, 10_13, 8_0, 11_0, "use LAErrorBiometryNotEnrolled") = kLAErrorTouchIDNotEnrolled,

// 验证不成功,因为有太多的失败的Touch ID尝试和触摸

// Touch ID是锁着的,解锁TouchID必须要使用密码
// 例如调用 LAPolicyDeviceOwnerAuthenticationWithBiometrics 的时候密码是必要的
LAErrorTouchIDLockout NS_ENUM_DEPRECATED(10_11, 10_13, 9_0, 11_0, "use LAErrorBiometryLockout")
__WATCHOS_DEPRECATED(3.0, 4.0, "use LAErrorBiometryLockout") __TVOS_DEPRECATED(10.0, 11.0, "use LAErrorBiometryLockout") = kLAErrorTouchIDLockout,

// 应用程序取消了身份验证(例如在进行身份验证时调用了无效)
LAErrorAppCancel API_AVAILABLE(macos(10.11), ios(9.0)) = kLAErrorAppCancel,

// LAContext传递给这个调用之前已经失效
LAErrorInvalidContext API_AVAILABLE(macos(10.11), ios(9.0)) = kLAErrorInvalidContext,

// 身份验证无法启动,因为生物识别验证在当前这个设备上不可用
LAErrorBiometryNotAvailable API_AVAILABLE(macos(10.13), ios(11.0)) API_UNAVAILABLE(watchos, tvos) = kLAErrorBiometryNotAvailable,

// 身份验证无法启动,因为生物识别没有录入信息
LAErrorBiometryNotEnrolled API_AVAILABLE(macos(10.13), ios(11.0)) API_UNAVAILABLE(watchos, tvos) = kLAErrorBiometryNotEnrolled,

// 身份验证不成功,因为太多次的验证失败并且生物识别验证是锁定状态。此时,必须输入密码才能解锁。例如LAPolicyDeviceOwnerAuthenticationWithBiometrics时候将密码作为先决条件。
LAErrorBiometryLockout API_AVAILABLE(macos(10.13), ios(11.0)) API_UNAVAILABLE(watchos, tvos) = kLAErrorBiometryLockout,

// 身份验证失败。因为这需要显示UI已禁止使用interactionNotAllowed属性。据说是beta版本
LAErrorNotInteractive API_AVAILABLE(macos(10.10), ios(8.0), watchos(3.0)) API_UNAVAILABLE(tvos) = kLAErrorNotInteractive,

// 无法启动身份验证,因为附近没有配对的手表设备。
LAErrorWatchNotAvailable API_AVAILABLE(macos(10.15)) API_UNAVAILABLE(ios, watchos, tvos) = kLAErrorWatchNotAvailable,

// 无法启动身份验证,因为此设备仅通过可移动配件支持生物识别,并且没有配对配件。
LAErrorBiometryNotPaired API_AVAILABLE(macos(11.2)) API_UNAVAILABLE(ios, watchos, tvos) = kLAErrorBiometryNotPaired,

// 无法启动身份验证,因为此设备仅通过可移动配件支持生物识别,并且配对的配件未连接。
LAErrorBiometryDisconnected API_AVAILABLE(macos(11.2)) API_UNAVAILABLE(ios, watchos, tvos) = kLAErrorBiometryDisconnected,

// 无法启动身份验证,因为嵌入式UI的尺寸无效。
LAErrorInvalidDimensions API_AVAILABLE(macos(12.0)) API_UNAVAILABLE(ios, watchos, tvos) = kLAErrorInvalidDimensions,

  1. evaluatePolicy

evaluatePolicy 是对生物识别进行验证,返回 YES 验证成功,返回 NO 验证失败。

  1. LABiometryType

这个属性是在 iOS11.0 新增额属性,用来判断是 TouchID 还是 FaceID,一般是在生物识别验证成功后才会去判断。

TouchID & FaceID 实际项目中的使用

  1. 封装 ZJTouchFaceManager 工具类

ZJTouchFaceManager.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
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
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSInteger, BiometricsSupportType) {
BiometricsType_None = 0, // 既不支持指纹也不支持面部识别
BiometricsType_TouchID, // 指纹解锁
BiometricsType_FaceID // 面部识别
};

typedef NS_ENUM(NSInteger, BiometricsResult) {
// 当前设备不支持生物识别验证
Biometrics_NotSupport = 0,
// 系统版本不支持生物识别 身份验证 (必须高于iOS 8.0才能使用)"
Biometrics_SystemVersionNotSupport,
// 生物识别 身份验证成功
Biometrics_Success,
// 生物识别 身份验证失败
Biometrics_Failure,
// 生物识别 身份验证被用户取消
Biometrics_UserCancel,
// 用户不使用生物识别身份验证,选择手动输入密码
Biometrics_InputPassword,
// 生物识别 身份验证被系统取消(如遇到来电,锁屏,按了Home键等)
Biometrics_SystemCancel,
// 生物识别 身份验证无法启动,因为用户没有设置密码
Biometrics_PasswordNotSet,
// 生物识别 身份验证无法启动,因为用户没有登记手指TouchID
Biometrics_TouchIDNotEnrolled,
// 生物识别 身份验证无法启动,因为TouchID不可用在设备上
Biometrics_TouchIDNotAvailable,
// TouchID 被锁定(连续多次验证TouchID失败,系统需要用户手动输入密码)
Biometrics_Lockout,
// 当前软件被挂起并取消了授权 (如App进入了后台等)
Biometrics_AppCancel,
// 当前软件被挂起并取消了授权 (LAContext对象无效)
Biometrics_InvalidContext,
};

typedef void(^BiometricsResultBlock)(BiometricsResult result,NSString * errorMsg);

@interface ZJTouchFaceManager : NSObject
// 单例
+ (instancetype)manager;

// 判断设备支持哪种生物识别认证方式 TouchID Or FaceID
- (BiometricsSupportType)checkBiometricsSupportType;

/**
启动生物验证
@param touchIDdesc 指纹识别文本描述
@param faceIDDesc 面部识别文本描述
@param block 回调结果的block
*/
- (void)startBiometricsWithTouchIDDescribe:(NSString * _Nullable)touchIDdesc FaceIDDescribe:(NSString * _Nullable)faceIDDesc ResultBlock:(BiometricsResultBlock)block;
@end

NS_ASSUME_NONNULL_END

ZJTouchFaceManager.m 中:

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#import "ZJTouchFaceManager.h"
#import <LocalAuthentication/LocalAuthentication.h>

@interface ZJTouchFaceManager ()
@property (nonatomic,strong)LAContext * context;
@end
@implementation ZJTouchFaceManager
// 单例
+ (instancetype)manager{
static ZJTouchFaceManager * manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[ZJTouchFaceManager alloc] init];
});
return manager;
}

// 判断设备支持哪种生物识别认证方式 TouchID Or FaceID
- (BiometricsSupportType)checkBiometricsSupportType{
BiometricsSupportType supportType = BiometricsType_None;

// 检测设备系统是否支持TouchID或者FaceID
if (@available(iOS 8.0, *)) {
// 初始化 LAContext 上下文对象
LAContext * context = [[LAContext alloc] init];
// 初始化错误对象指针
NSError * error = nil;
// 判断是否支持生物识别
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
// iOS 11.0 及以上包含指纹及面部识别的机型
if (@available(iOS 11.0, *)) {
// FaceID
if (context.biometryType == LABiometryTypeFaceID) {
supportType = BiometricsType_FaceID;
}
// TouchID
else if (context.biometryType == LABiometryTypeTouchID){
supportType = BiometricsType_TouchID;
}
}else {
/*
iOS 11.0 以下不包括面部识别
因为iPhoneX起始系统版本都已经是iOS11.0,所以iOS11.0系统版本下不需要再去判断是否支持faceID,直接走支持TouchID逻辑即可。
*/
// TouchID
supportType = BiometricsType_TouchID;
}
}
}else { // 既不支持TouchID 也不支持FaceID
supportType = BiometricsType_None;
}
return supportType;
}

/**
启动生物验证
@param touchIDDesc 指纹识别文本描述
@param faceIDDesc 面部识别文本描述
@param block 回调结果的block
*/
- (void)startBiometricsWithTouchIDDescribe:(NSString * _Nullable)touchIDDesc FaceIDDescribe:(NSString * _Nullable)faceIDDesc ResultBlock:(BiometricsResultBlock)block{
// 错误
NSError * error;

// 系统版本
if (NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0) {
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"系统版本不支持生物识别 身份验证 (必须高于iOS 8.0才能使用)");
block(Biometrics_SystemVersionNotSupport,error.localizedDescription);
});
return;
}
// 创建安全验证对象
LAContext * context = [[LAContext alloc] init];
/*
验证弹框提供两个按钮:
CancelButton 点击取消,FallbackButton 点击输入数字密码,可自定义标题
首次验证失败后才会出现 FallbackButton
*/
if (@available(iOS 10.0, *)) {
context.localizedCancelTitle = @"取消";
} else {
// Fallback on earlier versions
}
context.localizedFallbackTitle = @"输入密码验证";

// iOS 8.0 设备所有者使用生物识别方法进行验证
LAPolicy policyType = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
if (@available(iOS 9.0, *)) {
policyType = LAPolicyDeviceOwnerAuthentication;
}

// 生物识别 身份验证
if ([context canEvaluatePolicy:policyType error:&error]) {
// 支持类型
BiometricsSupportType supportType = [self checkBiometricsSupportType];

// 文本描述
NSString * desc;
if(supportType == BiometricsType_TouchID){
desc = touchIDDesc;
}else if(supportType == BiometricsType_FaceID){
desc = faceIDDesc;
}

// 开始验证
[context evaluatePolicy:policyType localizedReason:desc reply:^(BOOL success, NSError * _Nullable error) {
// 考虑到有可能没有授权开启-需再次验证是否支持 面容/指纹 生物识别认证
BiometricsSupportType supportType = [self checkBiometricsSupportType];
// 成功 并且 支持
if (success && supportType != BiometricsType_None) {
dispatch_async(dispatch_get_main_queue(), ^{
DLogInfo(@"生物识别 身份验证成功");
block(Biometrics_Success,error.localizedDescription);
});
}else {
ZJLog(@"生物识别 身份验证失败");
[self handleWithTouchIdOrFaceIdError:error ResultBlock:block];
}
}];
}else {
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"设备不支持生物识别 身份验证");
block(Biometrics_NotSupport,error.localizedDescription);
});

}
}

// 处理验证TouchID或FaceID失败
- (void)handleWithTouchIdOrFaceIdError:(NSError *)error ResultBlock:(BiometricsResultBlock)block{
switch (error.code) {
case LAErrorAuthenticationFailed:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"生物识别 身份验证失败,因为用户无法提供有效的凭据");
block(Biometrics_Failure,error.localizedDescription);
});
}
break;
case LAErrorUserCancel:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"生物识别 身份验证被用户取消(例如了取消按钮)");
block(Biometrics_UserCancel,error.localizedDescription);
});
}
break;
case LAErrorUserFallback:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"生物识别 身份验证被取消了,用户选择手动输入密码");
block(Biometrics_InputPassword,error.localizedDescription);
});
}
break;
case LAErrorSystemCancel:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"生物识别 身份验证被系统取消了(如遇到来电,锁屏,按了Home键等)");
block(Biometrics_SystemCancel,error.localizedDescription);
});
}
break;
case LAErrorPasscodeNotSet:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"生物识别 身份验证无法启动,因为用户没有设置密码");
block(Biometrics_PasswordNotSet,error.localizedDescription);
});
}
break;
case LAErrorTouchIDNotEnrolled:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"生物识别 身份验证无法启动,因为用户没有登记手指TouchID");
block(Biometrics_TouchIDNotEnrolled,error.localizedDescription);
});

}
break;
case LAErrorTouchIDNotAvailable:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"生物识别 身份验证无法启动,因为TouchID不可用在设备上");
block(Biometrics_TouchIDNotAvailable,error.localizedDescription);
});
}
break;
case LAErrorTouchIDLockout:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"TouchID 被锁定(连续多次验证TouchID失败,系统需要用户手动输入密码)");
block(Biometrics_Lockout,error.localizedDescription);
});
}
break;
case LAErrorAppCancel:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"当前软件被挂起并取消了授权 (如App进入了后台等)");
block(Biometrics_AppCancel,error.localizedDescription);
});
}
break;
case LAErrorInvalidContext:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"当前软件被挂起并取消了授权 (LAContext对象无效)");
block(Biometrics_InvalidContext,error.localizedDescription);
});
}
break;
default:{
dispatch_async(dispatch_get_main_queue(), ^{
ZJLog(@"生物识别 身份验证失败,因为用户无法提供有效的凭据");
block(Biometrics_Failure,@"生物识别 身份验证失败");
});
}
break;
}

}
@end
  1. 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 处理点击面容登录按钮的事件
- (void)handleClickFaceIdLoginBtn{
BiometricsSupportType supportType =[[ZJTouchFaceManager manager] checkBiometricsSupportType];
if (supportType == BiometricsType_FaceID) {
// 支持面容登录,未被用户在设置中关闭
[[ZJTouchFaceManager manager] startBiometricsWithTouchIDDescribe:@"" FaceIDDescribe:@"面容ID" ResultBlock:^(BiometricsResult result, NSString * _Nonnull errorMsg) {
if (result == Biometrics_Success) {
ZJLog(@"面容ID成功,可以登录了");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self startLogin];
});
}else {
ZJLog(@"面容ID失败");
[SVPManager showFailureAndStatus:errorMsg];
}
}];
}else {
[[ZJAlertView singleManager] setAlertViewType:ZJAlertViewType_FaceID];
[[ZJAlertView singleManager] show];
}
}

Gesture

iOS系统目前没有提供对外调取的API,因此,如果想实现 手势登录,那么我们就需要去手动绘制一个手势登录视图

  1. 创建 GestureV 视图

GestureV.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
29
30
31
32
33
34
35
36
37
38
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface GestureV : UIView
/* 颜色-用在按钮时时获取颜色 */
// 大圆填充色
@property (nonatomic,strong)UIColor * bigCircleFillColor;
// 小圆填充色
@property (nonatomic,strong)UIColor * smallCircirFillColor;
// 线颜色
@property (nonatomic,strong)UIColor * lineColor;

// 输入的次数
@property (nonatomic,assign)NSInteger inputNum;

/* 手势用处-初始化视图时调用*/
@property (nonatomic,assign)GestureUseType gestureUseType;

/* block回调 */
// 开始返回手势密码
@property (nonatomic,copy)void(^startInputPwdBlock)(NSMutableArray * btnArr,GestureBtnState btnState);
// 实时返回手势密码
@property (nonatomic,copy)void(^inputPasswordBlock)(NSMutableArray * btnArr,GestureBtnState btnState);
// 手势密码小于等于四位数
@property (nonatomic,copy)void(^errorInputBlock)(void);
// 设置手势密码:第一次成功
@property (nonatomic,copy)void(^setPasswordFirstSuccessBlock)(void);
// 设置手势密码:两次密码不一致
@property (nonatomic,copy)void(^setPasswordInconformityBlock)(void);

/* 设置手势按钮的状态 */
- (void)setPropertiesByState:(GestureBtnState)btnState;


@end

NS_ASSUME_NONNULL_END

Gesture.m中:

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
#import "GestureV.h"
#import "GestureConfig.h"
#import "GestureBtn.h"
// 网络请求
#import "LoginNetwork.h"

@interface GestureV ()
// 当前处于哪个按钮范围内
@property (nonatomic,assign)CGPoint currentPoint;
// 存储已选择的按钮
@property (nonatomic,strong)NSMutableArray * selectArr;
// 设置密码时第一次输入的手势密码
@property (nonatomic,copy)NSString * firstPassword;
@end
@implementation GestureV
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = kClearColor;
[self initUI];
}
return self;
}

// 初始化UI
- (void)initUI{
// 设置按钮的状态
[self setPropertiesByState:GestureBtnState_Normal];
// 按钮的大小
NSInteger btn_size = [GestureConfig sharedInstance].bigCircleRadius * 2;
// 内间距
CGFloat padding = (self.width - btn_size * 3) / 2.0;
for (int i = 0; i < 9; i++) {
// 行
NSInteger row = i / 3;
// 列
NSInteger col = i % 3;
// 创建按钮
GestureBtn * gestureBtn = [[GestureBtn alloc] initWithFrame:CGRectMake(col * btn_size + col * padding, row * btn_size + row * padding, btn_size, btn_size)];
[gestureBtn setTag:i+1];
[self addSubview:gestureBtn];
}
}

#pragma mark - 设置手势按钮的状态
// 设置按钮的状态
- (void)setPropertiesByState:(GestureBtnState)btnState{
switch (btnState) {
case GestureBtnState_Normal:
{
[self setUserInteractionEnabled:YES];
// 重置按钮
[self resetButtons];
// 大圆填充色
self.bigCircleFillColor = [GestureConfig sharedInstance].bigCircleFillColorNormal;
// 小圆填充色
self.smallCircirFillColor = [GestureConfig sharedInstance].smallCircleFillColorNormal;
// 线颜色
self.lineColor = [GestureConfig sharedInstance].lineColorNormal;
}
break;
case GestureBtnState_Selected:
{
[self setUserInteractionEnabled:YES];
self.bigCircleFillColor = [GestureConfig sharedInstance].bigCircleFillColorSelected;
self.smallCircirFillColor = [GestureConfig sharedInstance].smallCircleFillColorSelected;
self.lineColor = [GestureConfig sharedInstance].lineColorSelected;
}
break;
case GestureBtnState_Incorrect:
{
[self setUserInteractionEnabled:NO];
self.bigCircleFillColor = [GestureConfig sharedInstance].bigCircleFillColorIncorrect;
self.smallCircirFillColor = [GestureConfig sharedInstance].smallCircleFillColorIncorrect;
self.lineColor = [GestureConfig sharedInstance].lineColorIncorrect;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 恢复正常
[self setPropertiesByState:GestureBtnState_Normal];
});
}
break;
default:
break;
}
}

// 重置按钮
- (void)resetButtons{
for (NSInteger i = 0; i < self.selectArr.count; i++) {
GestureBtn * btn = self.selectArr[i];
[btn setSelected:NO];
}
[self.selectArr removeAllObjects];
[self setNeedsDisplay];
}

#pragma mark - 绘制线
- (void)drawRect:(CGRect)rect{
[super drawRect:rect];
// 如果选中按钮数量为0
if ([self.selectArr count] == 0) {
return;
}
// 绘制贝塞尔线
UIBezierPath * path = [UIBezierPath bezierPath];
// 线宽
[path setLineWidth:[GestureConfig sharedInstance].lineWidth];

// 如果显示手势轨迹
BOOL isShowGestureTrack = [[NSUserDefaults cacheGetObjectForKey:JITISShowGESTURETRACK] boolValue];
if (isShowGestureTrack) {
// 线颜色
[self.lineColor set];
}else {
// 线颜色
[kClearColor set];
}

// 设置头尾相接处的样式
[path setLineJoinStyle:kCGLineJoinRound];
// 设置头尾的样式
[path setLineCapStyle:kCGLineCapRound];
// 遍历按钮数组
for (NSInteger i = 0; i < self.selectArr.count; i ++) {
GestureBtn * btn = self.selectArr[i];
if (i == 0) {
[path moveToPoint:[btn center]];
}else{
[path addLineToPoint:[btn center]];
}
[btn setNeedsDisplay];
}
[path addLineToPoint:self.currentPoint];
[path stroke];
}


#pragma mark - touches事件
// 开始触摸
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];

UITouch * touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
self.currentPoint = point;
for (GestureBtn * btn in self.subviews) {
if (CGRectContainsPoint(btn.frame, point)) {
[btn setSelected:YES];
if (![self.selectArr containsObject:btn]) {
[self.selectArr addObject:btn];
[self setPropertiesByState:GestureBtnState_Selected];
if (self.gestureUseType != GestureUseType_Login) {
// 开始返回输入密码
self.startInputPwdBlock(self.selectArr,GestureBtnState_Selected);
}
}
}
}
[self setNeedsDisplay];
}
// 触摸移动
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesMoved:touches withEvent:event];
UITouch * touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
self.currentPoint = point;
for (GestureBtn * btn in self.subviews) {
if (CGRectContainsPoint(btn.frame, point)) {
[btn setSelected:YES];
if (![self.selectArr containsObject:btn]) {
[self.selectArr addObject:btn];
[self setPropertiesByState:GestureBtnState_Selected];
if (self.gestureUseType != GestureUseType_Login) {
// 实时返回输入密码
self.inputPasswordBlock(self.selectArr, GestureBtnState_Selected);
}
}
}
}
if (self.selectArr.count > 0) {

}
[self setNeedsDisplay];
}

// 触摸结束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesEnded:touches withEvent:event];

// 用于登录
if (self.gestureUseType == GestureUseType_Login) {
[self handleUseLogin];
}
// 用于设置手势密码
else if (self.gestureUseType == GestureUseType_SetPwd) {
[self handleUseSetPwd];
}
// 用于修改手势密码
else if (self.gestureUseType == GestureUseType_Modify) {
[self handleUseModifyPwd];
}

// 防止绘制出边界
GestureBtn * btn = [self.selectArr lastObject];
[self setCurrentPoint:btn.center];
[self setNeedsDisplay];
}

#pragma mark -处理用于登录的事件
- (void)handleUseLogin{
// 手势密码位数 <= 4 位
if (self.selectArr.count <= 4) {
self.errorInputBlock();
[self setPropertiesByState:GestureBtnState_Incorrect];
}else {
// 获取缓存的密码
NSString * gesturePwd = [NSUserDefaults cacheGetObjectForKey:JITGESTUREPASSWORD];
// 输入密码
NSString * inputPassword = [[NSString alloc] init];
for (GestureBtn *btn in self.selectArr) {
inputPassword = [inputPassword stringByAppendingFormat:@"%@",@(btn.tag)];
}
// 如果密码匹配
if ([inputPassword isEqualToString:gesturePwd]) {
[self setPropertiesByState:GestureBtnState_Normal];
// 开始登录
[self startLogin];
}else{ // 密码不匹配
[self setPropertiesByState:GestureBtnState_Incorrect];
}
}
}

// 开始登录
- (void)startLogin{
NSMutableArray * mArr = [ZJKeyChainManager readLoginInfoFromKeyChain];
if (mArr.count >= 3) {
// 企业ID
NSString * enterpriseId = [mArr objectAtIndex:0];
// 账号
NSString * account = [mArr objectAtIndex:1];
// 密码
NSString * password = [mArr objectAtIndex:2];
// 登录
[LoginNetwork loginWithEnterpriseId:enterpriseId account:account password:password success:^(int result) {
if (result != 0) {
[SVPManager showFailureAndStatus:@"手势登录失败,请重试"];
}
}];
}else {
[SVPManager showWithStatus:@"手势登录已过期,请使用密码登录"];
// 缓存登录类型-密码登录
[NSUserDefaults cacheSetInteger:LoginType_Password forKey:JITLOGINTYPE];
// 删除手势密码
[NSUserDefaults cacheSetObject:@"" forKey:JITGESTUREPASSWORD];
// 删除钥匙串登录信息
[ZJKeyChainManager deleteLoginInfoFromKeyChain];
}
}

#pragma mark -处理用于设置密码
- (void)handleUseSetPwd{
// 手势密码位数 <= 4 位
if (self.selectArr.count <= 4) {
// 错误输入
self.errorInputBlock();

// 实时输入状态
self.inputPasswordBlock(self.selectArr, GestureBtnState_Incorrect);

// 设置按钮状态为错误
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setPropertiesByState:GestureBtnState_Incorrect];
});
}else {
if (self.inputNum == 0) { // 第一次输入
// 赋值 第一次 手势密码
self.firstPassword = [[NSString alloc] init];
for (GestureBtn * btn in self.selectArr) {
self.firstPassword = [self.firstPassword stringByAppendingFormat:@"%@",@(btn.tag)];
}

// 次数+1
self.inputNum += 1;

// 设置密码第一次成功
self.setPasswordFirstSuccessBlock();

// 实时返回输入密码
self.inputPasswordBlock(self.selectArr, GestureBtnState_Normal);

// 恢复手势按钮状态-正常
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setPropertiesByState:GestureBtnState_Normal];
});
}else { // 第二次输入
// 赋值第二次输入密码
NSString * secondPassword = [[NSString alloc] init];
for (GestureBtn * btn in self.selectArr) {
secondPassword = [secondPassword stringByAppendingFormat:@"%@",@(btn.tag)];
}

// 如果 第一次 和 第二次 手势密码 相等
if ([self.firstPassword isEqualToString:secondPassword]) {
ZJLog(@"设置手势密码成功");
// 提示框
[SVPManager showSuccessAndStatus:@"手势密码设置成功"];

// 缓存手势密码
[NSUserDefaults cacheSetObject:secondPassword forKey:JITGESTUREPASSWORD];

// 缓存登录类型-手势登录
[NSUserDefaults cacheSetInteger:LoginType_Gesture forKey:JITLOGINTYPE];

// 缓存登录信息到钥匙串
[ZJKeyChainManager saveLoginInfoToKeyChain];

// 实时返回输入密码
self.inputPasswordBlock(self.selectArr, GestureBtnState_Normal);

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 恢复正常
[self setPropertiesByState:GestureBtnState_Normal];
// 返回上一个页面
[JITAppleDelegate.naviController popViewControllerAnimated:YES];
});

}else{ // 不相等
ZJLog(@"请重新设置密码");
// 重置次数
self.inputNum = 0;

if (self.gestureUseType != GestureUseType_Login) {
// 两次不一致的回调
self.setPasswordInconformityBlock();

// 实时返回输入密码
self.inputPasswordBlock(self.selectArr, GestureBtnState_Incorrect);
}

// 设置按钮状态为错误
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setPropertiesByState:GestureBtnState_Incorrect];
});
}
}
}
}

#pragma mark - 处理用于修改手势密码
- (void)handleUseModifyPwd{
// 手势密码位数 <= 4 位
if (self.selectArr.count <= 4) {

// 错误输入
self.errorInputBlock();

// 实时输入状态
self.inputPasswordBlock(self.selectArr, GestureBtnState_Incorrect);

// 设置按钮状态为错误
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setPropertiesByState:GestureBtnState_Incorrect];
});
}else {
if (self.inputNum == 0) { // 第一次输入
// 赋值 第一次 输入 密码
self.firstPassword = [[NSString alloc] init];
for (GestureBtn * btn in self.selectArr) {
self.firstPassword = [self.firstPassword stringByAppendingFormat:@"%@",@(btn.tag)];
}

// 次数+1
self.inputNum += 1;

// 设置密码第一次成功
self.setPasswordFirstSuccessBlock();

// 实时返回输入密码
self.inputPasswordBlock(self.selectArr, GestureBtnState_Normal);

// 恢复正常
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setPropertiesByState:GestureBtnState_Normal];
});
}else { // 第二次输入
// 赋值 第二次 手势密码
NSString * secondPassword = [[NSString alloc] init];
for (GestureBtn * btn in self.selectArr) {
secondPassword = [secondPassword stringByAppendingFormat:@"%@",@(btn.tag)];
}

// 如果第一次 和 第二次相等
if ([self.firstPassword isEqualToString:secondPassword]) {
ZJLog(@"修改密码成功");
// 提示框
[SVPManager showSuccessAndStatus:@"手势密码修改成功"];

// 缓存手势密码
[NSUserDefaults cacheSetObject:secondPassword forKey:JITGESTUREPASSWORD];

// 实时返回输入密码
self.inputPasswordBlock(self.selectArr, GestureBtnState_Normal);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 恢复正常
[self setPropertiesByState:GestureBtnState_Normal];
// 返回上一个页面
[JITAppleDelegate.naviController popViewControllerAnimated:YES];
});

}else{ // 不相等
ZJLog(@"请重新设置密码");
// 重置次数
self.inputNum = 0;
// 两次不一致的回调
self.setPasswordInconformityBlock();

// 实时返回输入密码
self.inputPasswordBlock(self.selectArr, GestureBtnState_Incorrect);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 设置按钮状态为错误
[self setPropertiesByState:GestureBtnState_Incorrect];
});
}
}
}
}

#pragma mark -懒加载
- (NSMutableArray *)selectArr{
if (!_selectArr) {
_selectArr = @[].mutableCopy;
}
return _selectArr;
}
@end
  1. 创建 GestureBtn 按钮

Gesture.h中:

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface GestureBtn : UIButton

@end

NS_ASSUME_NONNULL_END

Gesture.m中:

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
#import "GestureBtn.h"
#import "GestureV.h"
#import "GestureConfig.h"

@implementation GestureBtn
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
// 禁止交互
self.userInteractionEnabled = NO;
}
return self;
}

- (void)drawRect:(CGRect)rect{
[super drawRect:rect];

// 获取父视图
__weak GestureV * gestureV = nil;
if ([self.superview isKindOfClass:[GestureV class]]) {
gestureV = (GestureV *)self.superview;
}

// 获取画布
CGContextRef context = UIGraphicsGetCurrentContext();
// 中心点
CGPoint centerPoint = CGPointMake(rect.size.width/2, rect.size.height/2);
// 初始弧度
CGFloat startAngle = -((CGFloat)M_PI/2);
// 结束弧度
CGFloat endAngle = ((2 * (CGFloat)M_PI) + startAngle);

// 大圆半径
CGFloat radius = [GestureConfig sharedInstance].bigCircleRadius;
// 绘制大圆
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, startAngle, endAngle, 0);
// 大圆填充色
[gestureV.bigCircleFillColor set];
// 填充
CGContextFillPath(context);

// 绘制大圆圆弧
// CGContextAddArc(context, centerPoint.x, centerPoint.y, radius + [GestureConfig sharedInstance].strokeWidth/2, startAngle, endAngle, 0);
// 设置大圆弧颜色
// [gestureV.strokeColor setStroke];
// CGContextStrokePath(context);

// 如果显示小圆
if ([GestureConfig sharedInstance].showSmallCircle) {
// 绘制小圆
CGContextAddArc(context, centerPoint.x, centerPoint.y, [GestureConfig sharedInstance].smallCircleRadius, startAngle, endAngle, 0);
// 如果填充小圆
if ([GestureConfig sharedInstance].fillSmallCircle) {
// 小圆填充色
[gestureV.smallCircirFillColor set];
// 绘制填充
CGContextFillPath(context);
}else{
// 小圆填充色
[gestureV.smallCircirFillColor setStroke];
// 绘制弧线
CGContextStrokePath(context);
}
}
}

@end

  1. 创建 GestureConfig 类

GestureConfig.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface GestureConfig : NSObject
// 大圆半径
@property (nonatomic,assign)CGFloat bigCircleRadius;
// 小圆半径
@property (nonatomic, assign)CGFloat smallCircleRadius;
// 线宽度
@property (nonatomic, assign)CGFloat lineWidth;

/* 正常 */
// 圆弧填充色(正常)
@property (nonatomic,strong)UIColor * strokeFillColorNormal;
// 大圆填充色(正常)
@property (nonatomic,strong)UIColor * bigCircleFillColorNormal;
// 小圆填充色(正常)
@property (nonatomic,strong)UIColor * smallCircleFillColorNormal;
// 线颜色(正常)
@property (nonatomic,strong)UIColor * lineColorNormal;

/* 选择 */
// 大圆填充色(选择)
@property (nonatomic,strong)UIColor * bigCircleFillColorSelected;
// 小圆填充色(选择)
@property (nonatomic,strong)UIColor * smallCircleFillColorSelected;
// 线颜色(选择)
@property (nonatomic,strong)UIColor * lineColorSelected;

/* 错误 */
// 大圆填充色(错误)
@property (nonatomic,strong)UIColor * bigCircleFillColorIncorrect;
// 小圆填充色(错误)
@property (nonatomic,strong)UIColor * smallCircleFillColorIncorrect;
// 线颜色(错误)
@property (nonatomic,strong)UIColor * lineColorIncorrect;

// 是否显示小圆
@property (nonatomic,assign)BOOL showSmallCircle;
// 是否填充小圆
@property (nonatomic,assign)BOOL fillSmallCircle;

// 单例
+ (instancetype)sharedInstance;
@end

NS_ASSUME_NONNULL_END

GestureConfig.m中:

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
#import "GestureConfig.h"

@implementation GestureConfig
+ (instancetype)sharedInstance{
static GestureConfig * config = nil;
static dispatch_once_t oneToken;
dispatch_once(&oneToken, ^{
config = [GestureConfig new];

// 大圆半径
config.bigCircleRadius = 25.0f;
// 小圆半径
config.smallCircleRadius = 11.0f;
// 线宽度
config.lineWidth = 1.f;

/* 正常 */
// 圆弧填充色(正常)
// config.strokeFillColorNormal = [UIColor whiteColor];
// 大圆填充色(正常)
config.bigCircleFillColorNormal = [UIColor whiteColor];
// 小圆填充色(正常)
config.smallCircleFillColorNormal = kColorHexStr(@"#E8E9ED");
// 线颜色(正常)
config.lineColorNormal = [UIColor whiteColor];

/* 选择 */
// 大圆填充色(选择)
config.bigCircleFillColorSelected = kColorHexStrA(@"#567FFC", 0.1);
// 小圆填充色(选择)
config.smallCircleFillColorSelected = kColorHexStr(@"#567FFC");
// 线颜色(选择)
config.lineColorSelected = kColorHexStr(@"#567FFC");

/* 错误 */
// 大圆填充色(错误)
config.bigCircleFillColorIncorrect = kColorHexStrA(@"#CE4E4E", 0.1);
// 小圆填充色(错误)
config.smallCircleFillColorIncorrect = kColorHexStr(@"#CE4E4E");
// 线颜色(错误)
config.lineColorIncorrect = kColorHexStr(@"#CE4E4E");

// 是否显示小圆
config.showSmallCircle = YES;
// 是否填充小圆
config.fillSmallCircle = YES;
});
return config;
}
@end

  1. 枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 手势用处 */
typedef NS_ENUM(NSInteger, GestureUseType) {
GestureUseType_SetPwd = 0, // 设置密码
GestureUseType_Login, // 登录
GestureUseType_Modify, // 修改
};

/* 手势按钮状态 */
typedef NS_ENUM(NSInteger, GestureBtnState) {
GestureBtnState_Normal = 0, // 正常
GestureBtnState_Selected, // 选择
GestureBtnState_Incorrect, // 错误
};

  1. 使用
1
2
3
4
5
6
7
8
- (GestureV *)gestureV{
if (!_gestureV) {
_gestureV = [[GestureV alloc] initWithFrame:CGRectMake(0, 0, 250, 250)];
// 用处
_gestureV.gestureUseType = self.useType;
}
return _gestureV;
}
  • Post title:OC学习55:FaceID & TouchID & Guesture
  • Post author:张建
  • Create time:2023-05-30 16:57:12
  • Post link:https://redefine.ohevan.com/2023/05/30/OC/OC学习55:FaceID & TouchID & Guesture/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
On this page
OC学习55:FaceID & TouchID & Guesture