手势交互是iOS开发中用的比较多的一个类,用途无处不在,这里面也衍生了很多的需求和用法,UIGestureRecognizer很强大,它的子类包括很多,不过想要更完美的使用它,就需要了解它的底层原理和和一些特殊情况下的处理办法,本文主要介绍UITapGestureRecognizer的一些技巧性的方法和策略。

新添加的属性

UITapGestureRecognizer继承于UIGestureRecognizer,可以看到其中多添加了两个属性。

1
2
@property (nonatomic) NSUInteger numberOfTapsRequired; // Default is 1. The number of taps required to match
@property (nonatomic) NSUInteger numberOfTouchesRequired __TVOS_PROHIBITED; // Default is 1. The number of fingers required to match

numberOfTapsRequired 是需要点击的次数, numberOfTouchesRequired 是需要点击的手指数。使用起来也很容易,这里不过多的介绍。

继承于UIGestureRecognizer使用较多的API

在日常开发中,我们使用最多主要有以下几个API。

添加选择子

1
2
3
- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action NS_DESIGNATED_INITIALIZER; // designated initializer
- (void)addTarget:(id)target action:(SEL)action; // add a target/action pair. you can call this multiple times to specify multiple target/actions
- (void)removeTarget:(nullable id)target action:(nullable SEL)action; // remove the specified target/action pair. passing nil for target matches all targets, and the same for actions

给Tap添加选择子可以直接在UIGestureRecognizer初始化的时候添加,也可以在初始化完毕之后使用addTarget方法添加。使用removeTarget 可以移除对应的选择子。

获取点击点坐标

1
- (CGPoint)locationInView:(nullable UIView*)view; // a generic single-point location for the gesture. usually the centroid of the touches involved

这个方法用的很多,有时候根据不同的需求可以获取点击点的坐标进行相应的判断和处理,使用起来也很容易。

UIGestureRecognizerDelegate相关方法

如果需要实现一些特殊的需求,可能就需要使用代理方法,具体使用我会在待会进行讲解。先简单的了解一下下面两个方法。

1
2
// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;

从官方文档和注释我们可以看到上面这个代理方法会在点击事件发生之前被调用,返回NO的话就会阻止手势事件,然后执行点击事件,如果返回YES则表示不执行点击事件而是执行手势事件。

1
2
// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;

从官方文档和注释我们可以看到上面这个代理方法会在长按事件发生之前被调用,返回NO的话就会阻止手势事件,然后执行长按事件,如果返回YES则表示不执行长按事件而是执行手势事件。

使用技巧

首先需要知道事件响应的顺序,对于UIButton,UISlider,UISwitch等继承自UIControl的控件,都会率先响应事件,从而阻止了手势事件。手势也可以理解为一种特殊的层,所以在同一个View添加多个Tap手势,则执行最后一个Tap手势,对于TableView,Collection这样的弱点击事件,系统优先响应Tap事件,如果需要响应Cell的点击事件,就需要实现代理方法。下面是具体的例子。
这里写图片描述

子视图不需要响应Tap事件

相信这样的需求大家都遇到过,就是我在某个View里面添加了Tap事件,然后View里面添加了SecondView,我不想让SecondView响应Tap事件。怎么办呢,一种方法就是获取点击点的坐标,然后对比坐标是否在SecondView里面,这样做不是不可以,但是第一需要保证坐标系统不变,比如SecondView发生了旋转等可能会改变相关的坐标参考系,第二这个坐标还是会有一定的误差和出入,尤其是对于边界点的坐标。比较好的做法是在子视图上面也添加一个Tap,然后手势的选择子不做任何处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UITapGestureRecognizer *tapSuperGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapSuperView:)];
tapSuperGesture.delegate = self;
[self.view addGestureRecognizer:tapSuperGesture];
UITapGestureRecognizer *tapSecondGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapSecondView:)];
[self.secondView addGestureRecognizer:tapSecondGesture];
- (void)tapSuperView:(UITapGestureRecognizer *)gesture {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"点击了SuperView" delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
[alertView show];
}
- (void)tapSecondView:(UITapGestureRecognizer *)gesture {
}

这样根据优先响应的顺序,点击SecondView的时候tapSuperView 便不会响应。一些相关变化比如视图里面的某个区域不需要响应点击事件也可以用这样的方法解决,添加一个子视图进去就好。

响应TableViewCell事件

有时候会有这个蛋疼的需求,就是在视图上面加了一个TableView,然后视图添加了手势。这样就会出现UITapGesture和UITableViewCell的点击事件冲突。在前面我也介绍了需要实现委托方法来解决这个问题。只需要实现UIGestureRecognizerDelegate就可以解决这个问题。

1
2
3
4
5
@interface JQViewController ()<UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate>
@property (weak, nonatomic) IBOutlet UIView *secondView;
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
1
2
3
4
5
6
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return NO;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press {
return NO;
}

后一个方法可要可不要,不过有时候为了防止有的用户点击Cell喜欢一直压在上面可能出现未知的问题,所以还是把这个加上为好。
设置代理不要忘记tapSuperGesture.delegate = self;

TableViewCell上面需要添加Label手势事件

当然,最简单的方法就是把Label直接修改为Button,事件一定会被响应,不过在有些需求里面可能还是需要使用UILabel。这个时候会显得有些麻烦,不过方法还是有。可以把手势添加到TableView上面,然后通过判断坐标的方式。

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
- (void)tapTableView:(UITapGestureRecognizer *)gesture {
CGPoint location = [gesture locationInView:self.tableView];
//获得当前坐标对应的indexPath
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
if (indexPath) {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UILabel *label = nil;
for (UIView *view in cell.subviews) {
if ([view isKindOfClass:[UILabel class]]&& (view.tag == 1000)) {
label = (UILabel *)view;
}
}
//获取手势在当前Cell中位置
if (!label) return;
//判断手势在不在Lable中
if (CGRectContainsPoint(label.frame, point)) {
NSLog(@"点击了label");
}
}
}