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,更灵活。
  • 设计原则:生物识别和手势识别职责不同,建议分开封装为两个工具类,不要混在一个类中。

posted @ 2024-07-16 16:17  Mr.陳  阅读(400)  评论(0)    收藏  举报