3、点击手势:UITapGestureRecognizer 配置、多击与多指识别

点击手势,说白了就是 iOS 开发里最基础、最常用的手势。你想想看,用户点一下按钮、双击图片、三指同时点击……这些操作背后都是 UITapGestureRecognizer 在干活。我个人习惯把它叫做「轻点手势」,因为它确实很「轻」——配置简单,但坑也不少。

3.1 基础配置:一行代码搞定?没那么简单

先看个最基础的例子。给一个 UIView 添加单击手势:

UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] 
    initWithTarget:self action:@selector(handleTap:)];
[self.view addGestureRecognizer:tap];

嗯,确实就这几行。但我在项目中遇到过一个问题:手势和按钮的点击事件冲突了。你想想看,UIButton 本身就有点击响应,你再给它加个手势,有时候手势先响应了,按钮反而没反应。

避坑指南:我曾经在某个社交 App 里,给头像添加了点击手势来跳转个人主页。结果用户点击头像上的「关注」按钮时,手势先触发了,按钮事件被吞掉。后来我加了个判断:gestureRecognizer:shouldReceiveTouch: 代理方法里排除按钮区域。

配置时还有几个关键属性:

属性 类型 说明
numberOfTapsRequired NSInteger 点击次数,默认 1
numberOfTouchesRequired NSInteger 手指数量,默认 1
cancelsTouchesInView BOOL 是否取消视图的触摸事件,默认 YES
我的习惯:如果手势和视图自带事件共存,记得把 cancelsTouchesInView 设为 NO。这样手势识别后,视图还能继续收到触摸事件。

3.2 多击识别:双击、三击怎么配?

双击手势其实很简单——把 numberOfTapsRequired 改成 2 就行。但有个细节:双击和单击同时存在时,系统怎么区分?

举个例子,你想让用户单击选中图片,双击放大图片。如果只加两个手势,你会发现单击总是先触发。为什么会这样?因为手势识别有个延迟机制——系统要等一会儿,确认用户不会点第二下,才触发单击。

// 单击手势
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] 
    initWithTarget:self action:@selector(singleTap:)];
singleTap.numberOfTapsRequired = 1;

// 双击手势
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] 
    initWithTarget:self action:@selector(doubleTap:)];
doubleTap.numberOfTapsRequired = 2;

// 关键:单击需要等待双击失败
[singleTap requireGestureRecognizerToFail:doubleTap];

[self.view addGestureRecognizer:singleTap];
[self.view addGestureRecognizer:doubleTap];
核心要点:requireGestureRecognizerToFail: 是解决多击冲突的关键。它告诉系统:单击手势要等双击手势失败后,才能触发。这样用户双击时,单击就不会乱跳出来了。

我记得在做一个相册 App 时,用户反馈说「双击放大后,单击选中的逻辑乱了」。排查后发现是忘了加 requireGestureRecognizerToFail。加上后,体验立马正常了。

3.3 多指识别:三指同时点击怎么玩?

多指点击在游戏或特殊交互里比较常见。比如某些 App 的「开发者模式」需要三指同时点击才能进入。

UITapGestureRecognizer *threeFingerTap = [[UITapGestureRecognizer alloc] 
    initWithTarget:self action:@selector(threeFingerTap:)];
threeFingerTap.numberOfTapsRequired = 1;
threeFingerTap.numberOfTouchesRequired = 3;  // 三根手指

[self.view addGestureRecognizer:threeFingerTap];

这里有个坑:模拟器上怎么测试多指? 按住 Option 键,会出现两个触摸点。再按住 Shift 键可以移动它们。但三指就有点麻烦了——我一般直接用真机测。

注意:多指手势在 iPad 上比较常用,但 iPhone 屏幕小,三指同时点击容易误触。我曾经在某个工具类 App 里用了三指手势,结果用户经常在打字时触发。后来改成了「三指长按」才解决问题。

3.4 手势代理:精细控制触摸行为

有时候你需要更精细的控制。比如,某个区域不允许手势响应,或者手势和其他手势共存。这时候就要用 UIGestureRecognizerDelegate

// 代理方法:是否允许手势开始
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    // 获取触摸点
    CGPoint point = [gestureRecognizer locationInView:self.view];
    // 如果触摸点在某个按钮区域内,拒绝手势
    if (CGRectContainsPoint(self.specialButton.frame, point)) {
        return NO;
    }
    return YES;
}

// 代理方法:是否允许同时识别多个手势
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 
    shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;  // 允许同时响应
}
实用技巧:如果你想让手势和 UIScrollView 的滚动共存,记得实现 shouldRecognizeSimultaneouslyWithGestureRecognizer: 返回 YES。不然手势会吃掉滚动事件。

3.5 状态机与点击手势

每个手势识别器都有状态机。点击手势的状态变化很简单:

  • Possible:初始状态,等待识别
  • Recognized:成功识别为点击
  • Failed:识别失败(比如手指移动了)

你可以在回调里通过 gestureRecognizer.state 来判断当前状态。不过说实话,点击手势一般只用 UIGestureRecognizerStateRecognized 就够了。

嗯,这里要注意:点击手势不会进入 Began、Changed、Ended 这些连续状态。它只有「识别成功」和「识别失败」两种结果。如果你需要跟踪手指按下和抬起的细节,得用 UILongPressGestureRecognizer 或者直接重写 touchesBegan: 方法。

3.6 实战:一个完整的点击手势示例

最后,给一个我在项目中常用的封装。它支持单击、双击、三指点击,并且处理了冲突:

- (void)setupTapGestures {
    // 单击
    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] 
        initWithTarget:self action:@selector(handleSingleTap:)];
    singleTap.numberOfTapsRequired = 1;
    
    // 双击
    UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] 
        initWithTarget:self action:@selector(handleDoubleTap:)];
    doubleTap.numberOfTapsRequired = 2;
    
    // 三指单击
    UITapGestureRecognizer *threeFingerTap = [[UITapGestureRecognizer alloc] 
        initWithTarget:self action:@selector(handleThreeFingerTap:)];
    threeFingerTap.numberOfTouchesRequired = 3;
    
    // 设置依赖关系
    [singleTap requireGestureRecognizerToFail:doubleTap];
    
    // 设置代理
    singleTap.delegate = self;
    doubleTap.delegate = self;
    threeFingerTap.delegate = self;
    
    // 添加到视图
    [self.view addGestureRecognizer:singleTap];
    [self.view addGestureRecognizer:doubleTap];
    [self.view addGestureRecognizer:threeFingerTap];
}

#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 
    shouldReceiveTouch:(UITouch *)touch {
    // 排除按钮区域
    if ([touch.view isKindOfClass:[UIButton class]]) {
        return NO;
    }
    return YES;
}

这个封装我用了好几年,基本没出过问题。你想想看,把冲突处理、代理设置、依赖关系都放在一起,维护起来多省心。

总结一下:点击手势虽然简单,但多击、多指、冲突处理这些细节,稍不注意就会踩坑。我的建议是——先想清楚交互场景,再动手写代码。尤其是双击和单击共存时,requireGestureRecognizerToFail: 一定要加上。

好了,这一章就到这里。下一章我们会聊长按手势,那个比点击手势复杂多了——有状态变化、有触发时长、还有移动距离的判断。到时候我再分享几个实战踩坑经历。