iOS开发基础101-生物识别与手势识别:从 FaceID/TouchID
iOS 生物识别与手势识别:从 FaceID/TouchID 到 UIGestureRecognizer
FaceID/TouchID 为 App 提供了安全便捷的身份验证,手势识别则丰富了用户交互。本文从底层原理讲起,深入解析 LocalAuthentication 框架的安全机制、错误码处理、两种验证策略,以及 7 种手势识别器的用法和冲突处理,给出完整的 OC/Swift 封装代码和常见坑排查。
一、生物识别(FaceID / TouchID)
一句话原理
生物识别是用用户的指纹或面部特征代替密码进行身份验证,验证过程在设备本地的 Secure Enclave(安全隔区)中完成,App 拿不到指纹/面部数据,只能收到"成功"或"失败"的结果。
安全机制:Secure Enclave
- 指纹和面部数据加密存储在设备的 Secure Enclave 中,不会上传到苹果服务器,也不会被 App 获取。
- App 只能通过 LocalAuthentication 框架发起验证请求,由系统处理验证过程。
- 验证结果只有成功/失败,App 无法获取生物特征数据。
- 这是生物识别安全的核心保证。
设备支持情况
| 设备 | 生物识别类型 |
|---|---|
| iPhone X 及以后(无 Home 键) | FaceID |
| iPhone 8 及之前(有 Home 键) | TouchID |
| iPad Pro 11/12.9(第三代+) | FaceID |
| iPad Air/mini(有 Home 键) | TouchID |
| iPhone 15 Pro 及更新(部分) | 可能支持 OpticID( under-display TouchID) |
1. Info.plist 配置
FaceID 需要在 Info.plist 中添加使用说明,TouchID 不需要额外配置:
<!-- FaceID 使用说明(必须,否则调用时崩溃) -->
<key>NSFaceIDUsageDescription</key>
<string>需要使用 Face ID 验证您的身份,保护账户安全</string>
常见错误:不要添加
NSLocalAuthenticationUsageDescription这个 key——它不是有效的 Info.plist 键,系统不会识别。只有NSFaceIDUsageDescription是需要配置的。也不要重复添加同一个 key(重复的 key 只有最后一个生效)。
2. 核心 API
LAContext
LAContext 是生物识别的上下文对象,每次认证建议创建新的实例(不要复用全局实例,避免状态残留)。
LAPolicy(两种验证策略)
| 策略 | 说明 | 适用场景 |
|---|---|---|
deviceOwnerAuthenticationWithBiometrics |
只允许生物识别,失败后不自动弹出密码输入 | 严格要求生物识别的场景 |
deviceOwnerAuthentication |
先生物识别,失败后可以选择输入设备密码 | 大多数场景,用户体验更好 |
推荐用
deviceOwnerAuthentication,用户生物识别失败后还能用密码解锁,体验更友好。
biometryType(生物识别类型)
| 值 | 说明 |
|---|---|
.none |
不支持生物识别(或未调用 canEvaluatePolicy) |
.faceID |
支持 FaceID |
.touchID |
支持 TouchID |
.opticID |
支持 OpticID(iPhone 15 Pro+ 屏下指纹,iOS 17+) |
注意:
biometryType必须在调用canEvaluatePolicy:error:之后才有正确的值,调用之前是.none。
3. 错误码处理
生物识别失败时,error.code 对应不同的错误类型:
| 错误码 | 值 | 说明 |
|---|---|---|
LAErrorAuthenticationFailed |
-1 | 认证失败(指纹/面部不匹配) |
LAErrorUserCancel |
-2 | 用户取消 |
LAErrorUserFallback |
-3 | 用户选择输入密码(fallback) |
LAErrorSystemCancel |
-4 | 系统取消(如来电、按 Home 键) |
LAErrorPasscodeNotSet |
-5 | 设备未设置密码 |
LAErrorBiometryNotAvailable |
-6 | 生物识别不可用(设备不支持或被禁用) |
LAErrorBiometryNotEnrolled |
-7 | 未录入指纹/面部 |
LAErrorBiometryLockout |
-8 | 生物识别被锁定(多次失败,需输入密码解锁) |
LAErrorAppCancel |
-9 | App 主动取消(调用 invalidate) |
4. 完整封装(OC)
// BiometricAuthManager.h
#import <Foundation/Foundation.h>
#import <LocalAuthentication/LocalAuthentication.h>
typedef NS_ENUM(NSInteger, BiometricType) {
BiometricTypeNone,
BiometricTypeTouchID,
BiometricTypeFaceID,
BiometricTypeOpticID API_AVAILABLE(ios(17.0))
};
typedef void (^BiometricAuthCompletion)(BOOL success, NSString * _Nullable message, NSError * _Nullable error);
@interface BiometricAuthManager : NSObject
/// 检测设备支持的生物识别类型
+ (BiometricType)supportedBiometricType;
/// 发起生物识别认证
/// @param reason 提示原因(显示在验证框上)
/// @param allowFallback 是否允许 fallback 到密码
/// @param completion 回调(主线程)
+ (void)authenticateWithReason:(NSString *)reason
allowFallback:(BOOL)allowFallback
completion:(BiometricAuthCompletion)completion;
@end
// BiometricAuthManager.m
#import "BiometricAuthManager.h"
@implementation BiometricAuthManager
+ (BiometricType)supportedBiometricType {
LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
// 必须先调用 canEvaluatePolicy,biometryType 才有值
if (![context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
return BiometricTypeNone;
}
if (@available(iOS 17.0, *)) {
if (context.biometryType == LABiometryTypeOpticID) {
return BiometricTypeOpticID;
}
}
if (context.biometryType == LABiometryTypeFaceID) {
return BiometricTypeFaceID;
}
if (context.biometryType == LABiometryTypeTouchID) {
return BiometricTypeTouchID;
}
return BiometricTypeNone;
}
+ (void)authenticateWithReason:(NSString *)reason
allowFallback:(BOOL)allowFallback
completion:(BiometricAuthCompletion)completion {
// 每次认证创建新的 LAContext,不要复用
LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
// 选择验证策略
LAPolicy policy = allowFallback ? LAPolicyDeviceOwnerAuthentication : LAPolicyDeviceOwnerAuthenticationWithBiometrics;
// 检查是否支持
if (![context canEvaluatePolicy:policy error:&error]) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(NO, [self errorMessageForError:error], error);
});
return;
}
// 发起认证
[context evaluatePolicy:policy localizedReason:reason reply:^(BOOL success, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (success) {
completion(YES, @"认证成功", nil);
} else {
completion(NO, [self errorMessageForError:error], error);
}
});
}];
}
+ (NSString *)errorMessageForError:(NSError *)error {
switch (error.code) {
case LAErrorAuthenticationFailed:
return @"认证失败,请重试";
case LAErrorUserCancel:
return @"用户取消了认证";
case LAErrorUserFallback:
return @"用户选择输入密码";
case LAErrorSystemCancel:
return @"系统取消了认证";
case LAErrorPasscodeNotSet:
return @"设备未设置密码,无法使用生物识别";
case LAErrorBiometryNotAvailable:
return @"设备不支持生物识别";
case LAErrorBiometryNotEnrolled:
return @"未录入指纹/面部,请在系统设置中添加";
case LAErrorBiometryLockout:
return @"生物识别已被锁定,请输入设备密码解锁后重试";
case LAErrorAppCancel:
return @"应用取消了认证";
default:
return [NSString stringWithFormat:@"认证失败:%@", error.localizedDescription];
}
}
@end
5. 完整封装(Swift)
import LocalAuthentication
enum BiometricType {
case none, touchID, faceID, opticID
}
class BiometricAuthManager {
static var supportedType: BiometricType {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
return .none
}
if #available(iOS 17.0, *) {
if context.biometryType == .opticID { return .opticID }
}
switch context.biometryType {
case .faceID: return .faceID
case .touchID: return .touchID
default: return .none
}
}
static func authenticate(reason: String,
allowFallback: Bool = true,
completion: @escaping (Bool, String?, Error?) -> Void) {
let context = LAContext() // 每次创建新实例
var error: NSError?
let policy: LAPolicy = allowFallback ? .deviceOwnerAuthentication : .deviceOwnerAuthenticationWithBiometrics
guard context.canEvaluatePolicy(policy, error: &error) else {
DispatchQueue.main.async {
completion(false, errorMessage(for: error), error)
}
return
}
context.evaluatePolicy(policy, localizedReason: reason) { success, error in
DispatchQueue.main.async {
if success {
completion(true, "认证成功", nil)
} else {
completion(false, errorMessage(for: error), error)
}
}
}
}
private static func errorMessage(for error: Error?) -> String {
guard let error = error as? LAError else {
return error?.localizedDescription ?? "认证失败"
}
switch error.code {
case .authenticationFailed: return "认证失败,请重试"
case .userCancel: return "用户取消了认证"
case .userFallback: return "用户选择输入密码"
case .systemCancel: return "系统取消了认证"
case .passcodeNotSet: return "设备未设置密码"
case .biometryNotAvailable: return "设备不支持生物识别"
case .biometryNotEnrolled: return "未录入指纹/面部"
case .biometryLockout: return "生物识别已被锁定,请输入密码解锁"
case .appCancel: return "应用取消了认证"
default: return error.localizedDescription
}
}
}
二、手势识别(UIGestureRecognizer)
一句话原理
手势识别器是把一系列触摸事件(按下、移动、抬起)封装成高层语义(点击、滑动、捏合等),开发者不需要自己处理复杂的触摸逻辑,直接识别手势即可。
手势的状态机
每个手势识别器都有状态,随着触摸事件变化:
Possible(可能)→ Began(开始)→ Changed(变化中)→ Ended(结束)
↓
Cancelled(取消)
↓
Failed(失败)
| 状态 | 说明 |
|---|---|
Possible |
初始状态,正在识别中 |
Began |
手势开始识别(如手指开始移动) |
Changed |
手势持续变化中(如手指持续移动) |
Ended |
手势成功结束 |
Cancelled |
手势被取消(如来电中断) |
Failed |
手势识别失败 |
离散手势(如点击)只有 Ended 状态;连续手势(如平移、捏合)有 Began → Changed → Ended 完整流程。
7 种手势识别器
| 手势类 | 说明 | 类型 |
|---|---|---|
UITapGestureRecognizer |
点击(单击/双击/多击) | 离散 |
UILongPressGestureRecognizer |
长按 | 离散/连续 |
UIPinchGestureRecognizer |
捏合(缩放) | 连续 |
UIRotationGestureRecognizer |
旋转 | 连续 |
UIPanGestureRecognizer |
平移(拖动) | 连续 |
UISwipeGestureRecognizer |
轻扫(快速滑动) | 离散 |
UIScreenEdgePanGestureRecognizer |
屏幕边缘平移 | 连续 |
1. 点击手势(UITapGestureRecognizer)
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
tap.numberOfTapsRequired = 2; // 双击(默认 1 单击)
tap.numberOfTouchesRequired = 1; // 单指(默认 1)
[view addGestureRecognizer:tap];
2. 长按手势(UILongPressGestureRecognizer)
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
longPress.minimumPressDuration = 0.5; // 最短按 0.5 秒
longPress.allowableMovement = 10; // 允许移动 10 像素
[view addGestureRecognizer:longPress];
3. 捏合手势(UIPinchGestureRecognizer)
- (void)handlePinch:(UIPinchGestureRecognizer *)pinch {
if (pinch.state == UIGestureRecognizerStateChanged) {
pinch.view.transform = CGAffineTransformScale(pinch.view.transform, pinch.scale, pinch.scale);
pinch.scale = 1.0; // 重置,避免累积
}
}
4. 旋转手势(UIRotationGestureRecognizer)
- (void)handleRotation:(UIRotationGestureRecognizer *)rotation {
if (rotation.state == UIGestureRecognizerStateChanged) {
rotation.view.transform = CGAffineTransformRotate(rotation.view.transform, rotation.rotation);
rotation.rotation = 0; // 重置
}
}
5. 平移手势(UIPanGestureRecognizer)
- (void)handlePan:(UIPanGestureRecognizer *)pan {
CGPoint translation = [pan translationInView:pan.view.superview];
if (pan.state == UIGestureRecognizerStateChanged) {
pan.view.center = CGPointMake(pan.view.center.x + translation.x, pan.view.center.y + translation.y);
[pan setTranslation:CGPointZero inView:pan.view.superview]; // 重置
}
}
6. 轻扫手势(UISwipeGestureRecognizer)
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
swipe.direction = UISwipeGestureRecognizerDirectionLeft; // 向左轻扫
[view addGestureRecognizer:swipe];
7. 屏幕边缘平移(UIScreenEdgePanGestureRecognizer)
UIScreenEdgePanGestureRecognizer *edgePan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleEdgePan:)];
edgePan.edges = UIRectEdgeLeft; // 从左边缘开始
[view addGestureRecognizer:edgePan];
手势冲突处理
问题场景
一个 view 上同时添加了点击手势和双击手势,单击时会先触发单击,再触发双击——但我们希望双击时不触发单击。
解决方案一:requireGestureRecognizerToFail
// 单击手势需要等双击手势失败后才触发
[tap requireGestureRecognizerToFail:doubleTap];
解决方案二:手势代理
@interface ViewController () <UIGestureRecognizerDelegate>
@end
// 允许多个手势同时识别
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES; // 捏合和旋转可以同时进行
}
// 某个手势开始前是否可以识别
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
return YES;
}
手势与 UIButton 冲突
UIButton 自带点击事件,如果给 button 的 superview 添加了 tap 手势,点击 button 时可能两个都触发。解决方法:
// 手势识别器忽略对 UIControl 子视图的触摸
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if ([touch.view isKindOfClass:[UIControl class]]) {
return NO; // 不拦截按钮的点击
}
return YES;
}
手势工具类封装(OC)
// GestureHelper.h
#import <UIKit/UIKit.h>
typedef void (^GestureBlock)(UIGestureRecognizer *gesture);
@interface GestureHelper : NSObject
/// 添加点击手势(block 回调)
+ (UITapGestureRecognizer *)addTapToView:(UIView *)view
tapsRequired:(NSUInteger)taps
action:(GestureBlock)action;
/// 添加长按手势
+ (UILongPressGestureRecognizer *)addLongPressToView:(UIView *)view
duration:(NSTimeInterval)duration
action:(GestureBlock)action;
/// 添加平移手势
+ (UIPanGestureRecognizer *)addPanToView:(UIView *)view action:(GestureBlock)action;
/// 添加捏合手势
+ (UIPinchGestureRecognizer *)addPinchToView:(UIView *)view action:(GestureBlock)action;
/// 添加旋转手势
+ (UIRotationGestureRecognizer *)addRotationToView:(UIView *)view action:(GestureBlock)action;
/// 添加轻扫手势
+ (UISwipeGestureRecognizer *)addSwipeToView:(UIView *)view
direction:(UISwipeGestureRecognizerDirection)direction
action:(GestureBlock)action;
@end
// GestureHelper.m
#import "GestureHelper.h"
#import <objc/runtime.h>
static const void *kGestureBlockKey = &kGestureBlockKey;
@implementation GestureHelper
+ (UITapGestureRecognizer *)addTapToView:(UIView *)view tapsRequired:(NSUInteger)taps action:(GestureBlock)action {
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
tap.numberOfTapsRequired = taps;
[self attachBlock:action toGesture:tap];
[view addGestureRecognizer:tap];
return tap;
}
+ (UILongPressGestureRecognizer *)addLongPressToView:(UIView *)view duration:(NSTimeInterval)duration action:(GestureBlock)action {
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
longPress.minimumPressDuration = duration;
[self attachBlock:action toGesture:longPress];
[view addGestureRecognizer:longPress];
return longPress;
}
+ (UIPanGestureRecognizer *)addPanToView:(UIView *)view action:(GestureBlock)action {
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
[self attachBlock:action toGesture:pan];
[view addGestureRecognizer:pan];
return pan;
}
+ (UIPinchGestureRecognizer *)addPinchToView:(UIView *)view action:(GestureBlock)action {
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
[self attachBlock:action toGesture:pinch];
[view addGestureRecognizer:pinch];
return pinch;
}
+ (UIRotationGestureRecognizer *)addRotationToView:(UIView *)view action:(GestureBlock)action {
UIRotationGestureRecognizer *rotation = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
[self attachBlock:action toGesture:rotation];
[view addGestureRecognizer:rotation];
return rotation;
}
+ (UISwipeGestureRecognizer *)addSwipeToView:(UIView *)view direction:(UISwipeGestureRecognizerDirection)direction action:(GestureBlock)action {
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
swipe.direction = direction;
[self attachBlock:action toGesture:swipe];
[view addGestureRecognizer:swipe];
return swipe;
}
#pragma mark - Private
+ (void)attachBlock:(GestureBlock)block toGesture:(UIGestureRecognizer *)gesture {
objc_setAssociatedObject(gesture, kGestureBlockKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
+ (void)handleGesture:(UIGestureRecognizer *)gesture {
GestureBlock block = objc_getAssociatedObject(gesture, kGestureBlockKey);
if (block) block(gesture);
}
@end
用 block 回调比 target/action 更灵活,不需要在控制器中写一堆 @objc 方法。用
objc_setAssociatedObject把 block 关联到手势对象上。
三、使用示例
OC 示例
#import "ViewController.h"
#import "BiometricAuthManager.h"
#import "GestureHelper.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *demoView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 生物识别
BiometricType type = [BiometricAuthManager supportedBiometricType];
NSLog(@"支持的生物识别:%ld", (long)type);
[BiometricAuthManager authenticateWithReason:@"验证身份以查看敏感信息"
allowFallback:YES
completion:^(BOOL success, NSString *message, NSError *error) {
NSLog(@"%@", message);
if (success) {
// 显示敏感内容
}
}];
// 2. 点击手势(block 回调)
[GestureHelper addTapToView:self.demoView tapsRequired:1 action:^(UIGestureRecognizer *gesture) {
NSLog(@"点击了 view");
}];
// 3. 长按手势
[GestureHelper addLongPressToView:self.demoView duration:0.5 action:^(UIGestureRecognizer *gesture) {
if (gesture.state == UIGestureRecognizerStateBegan) {
NSLog(@"长按开始");
}
}];
// 4. 平移手势(拖动 view)
[GestureHelper addPanToView:self.demoView action:^(UIGestureRecognizer *gesture) {
UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gesture;
CGPoint translation = [pan translationInView:self.view];
if (pan.state == UIGestureRecognizerStateChanged) {
pan.view.center = CGPointMake(pan.view.center.x + translation.x, pan.view.center.y + translation.y);
[pan setTranslation:CGPointZero inView:self.view];
}
}];
// 5. 捏合 + 旋转(同时识别)
UIPinchGestureRecognizer *pinch = [GestureHelper addPinchToView:self.demoView action:^(UIGestureRecognizer *gesture) {
UIPinchGestureRecognizer *p = (UIPinchGestureRecognizer *)gesture;
if (p.state == UIGestureRecognizerStateChanged) {
p.view.transform = CGAffineTransformScale(p.view.transform, p.scale, p.scale);
p.scale = 1.0;
}
}];
UIRotationGestureRecognizer *rotation = [GestureHelper addRotationToView:self.demoView action:^(UIGestureRecognizer *gesture) {
UIRotationGestureRecognizer *r = (UIRotationGestureRecognizer *)gesture;
if (r.state == UIGestureRecognizerStateChanged) {
r.view.transform = CGAffineTransformRotate(r.view.transform, r.rotation);
r.rotation = 0;
}
}];
// 允许捏合和旋转同时识别
pinch.delegate = self;
rotation.delegate = self;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
@end
Swift 示例
class ViewController: UIViewController {
@IBOutlet weak var demoView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// 生物识别
BiometricAuthManager.authenticate(reason: "验证身份") { success, message, error in
print(message ?? "")
}
// 点击手势
let tap = UITapGestureRecognizer { [weak self] _ in
print("点击了")
}
demoView.addGestureRecognizer(tap)
// 平移手势
let pan = UIPanGestureRecognizer { [weak self] gesture in
guard let pan = gesture as? UIPanGestureRecognizer else { return }
let translation = pan.translation(in: self?.view)
if pan.state == .changed {
pan.view?.center.x += translation.x
pan.view?.center.y += translation.y
pan.setTranslation(.zero, in: self?.view)
}
}
demoView.addGestureRecognizer(pan)
}
}
// UIGestureRecognizer 的 block 扩展
extension UIGestureRecognizer {
convenience init(action: @escaping (UIGestureRecognizer) -> Void) {
self.init()
self.actionBlock = action
addTarget(self, action: #selector(handleAction(_:)))
}
private static var blockKey: UInt8 = 0
var actionBlock: ((UIGestureRecognizer) -> Void)? {
get { objc_getAssociatedObject(self, &Self.blockKey) as? (UIGestureRecognizer) -> Void }
set { objc_setAssociatedObject(self, &Self.blockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
@objc private func handleAction(_ sender: UIGestureRecognizer) {
actionBlock?(sender)
}
}
四、常见问题与坑
Q1:调用生物识别时崩溃了?
检查 Info.plist 中是否添加了 NSFaceIDUsageDescription。FaceID 设备上没有这个 key 会直接崩溃。TouchID 不需要配置。
Q2:biometryType 总是返回 none?
biometryType 必须在调用 canEvaluatePolicy:error: 之后才有正确的值。直接访问 context.biometryType 而不先调用 canEvaluatePolicy,会得到 .none。
Q3:LAContext 可以复用吗?
不建议。每次认证创建新的 LAContext 实例。复用可能导致 biometryType 缓存、上次认证状态残留、被系统 invalidate 后无法继续使用等问题。
Q4:生物识别被锁定了怎么办?
多次失败后生物识别会被锁定(LAErrorBiometryLockout),此时需要用户输入设备密码解锁。用 LAPolicyDeviceOwnerAuthentication 策略会自动提供密码输入选项。
Q5:捏合和旋转手势不能同时生效?
默认两个手势互斥,一个识别成功后另一个会失败。实现 UIGestureRecognizerDelegate 的 gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: 方法返回 YES,允许多个手势同时识别。
Q6:单击和双击冲突怎么办?
用 requireGestureRecognizerToFail:,让单击手势等双击手势失败后再触发:
[singleTap requireGestureRecognizerToFail:doubleTap];
Q7:手势和 UIButton 点击冲突?
给父视图添加的 tap 手势会拦截 button 的点击。用手势代理的 gestureRecognizer:shouldReceiveTouch: 方法,当触摸的 view 是 UIControl 时返回 NO,不拦截。
Q8:UISwipeGestureRecognizer 和 UIPanGestureRecognizer 冲突?
轻扫是快速滑动,平移是慢速拖动。可以让轻扫手势等平移手势失败,或者通过速度/位移判断。一般轻扫用于快速切换页面,平移用于拖动,根据场景选择。
Q9:手势的 block 会循环引用吗?
block 中引用了 self,而手势对象持有 block,view 持有手势,self 持有 view——形成循环引用。在 block 中用 __weak typeof(self) weakSelf = self 打破循环。
Q10:生物识别的结果可以被伪造吗?
不能。验证过程在 Secure Enclave 中完成,App 只能收到系统返回的成功/失败结果,无法伪造。但要注意:生物识别只能验证"这是设备主人",不能替代服务端的身份验证(如 token 校验)。
五、总结
- 生物识别:
- 原理:Secure Enclave 本地验证,App 拿不到生物数据。
- 配置:只需要
NSFaceIDUsageDescription(FaceID),TouchID 不需要。 - 策略:推荐
deviceOwnerAuthentication(允许 fallback 密码)。 - 错误码:9 种常见错误,逐一处理给用户友好提示。
- 注意:每次创建新的 LAContext,先 canEvaluatePolicy 再读 biometryType。
- 手势识别:
- 7 种手势:点击、长按、捏合、旋转、平移、轻扫、边缘平移。
- 状态机:Possible → Began → Changed → Ended/Cancelled/Failed。
- 冲突处理:requireGestureRecognizerToFail、手势代理、shouldReceiveTouch。
- 封装:用 block 回调替代 target/action,更灵活。
- 设计原则:生物识别和手势识别职责不同,建议分开封装为两个工具类,不要混在一个类中。

浙公网安备 33010602011771号