关于UIScrollView和UITableView手势冲突的解决方法

前段时间公司项目中遇到的问题,解决之后想着写篇博客总结一下但苦于一直没有时间,以致拖到了现在,马上过年了现在来还账。。。

项目中需要实现以下这种页面,需求:
1、在 UIScrollView 上嵌套左右两个 UITableView,以实现左右滑动展示页面的效果
2、UITableView 可执行左滑删除 cell 功能

需求示意图

然而在实现过程中发现 UIScrollView 的滚动和 UITableView 的左滑删除存在手势冲突,幸好解决方法并不复杂,不过在实现过程中还是很捉急的。

阶段一:实现 UITableView 左右滑动的效果

这个效果的实现还是很简单的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CGFloat viewWidth = self.view.frame.size.width;
CGFloat viewHeight = self.view.frame.size.height;

// scroll view
self.scrollView = [[MyScrollView alloc] initWithFrame:self.view.frame];
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.bounces = NO;
self.scrollView.pagingEnabled = YES;
self.scrollView.contentSize = CGSizeMake(viewWidth * 2, 0);
[self.view addSubview:self.scrollView];

// left table view
self.leftTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, viewWidth, viewHeight-160) style:UITableViewStylePlain];
self.leftTableView.backgroundColor = [UIColor whiteColor];
[self.scrollView addSubview:self.leftTableView];

// right table view
self.rightTableView = [[UITableView alloc] initWithFrame:CGRectMake(viewWidth, 0, viewWidth, viewHeight-160) style:UITableViewStylePlain];
[self.scrollView addSubview:self.rightTableView];

这个阶段可能遇到的情况也许是,如果当前页面是被前一个页面通过 navigationController push 出来的话,实现这种效果后可能会导致边缘触发返回手势实效,如果该手势失效应该检查:

1、系统边缘触发返回手势是否开启

1
self.navigationController.interactivePopGestureRecognizer.enabled = YES;

2、在该页面是否自定义了导航条的返回按钮

如果自定义了导航条的返回按钮,系统自带的边缘触发返回手势将会实效,解决方法是设置代理 UIGestureRecognizerDelegate,并设置:

1
self.navigationController.interactivePopGestureRecognizer.delegate = self;

然后重载其代理方法:

1
2
3
4
5
6
7
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
// 导航的 rootViewController 关闭右滑返回功能
if (self.navigationController.viewControllers.count <= 1) {
return NO;
}
return YES;
}

阶段二:实现 UITableView 左滑删除功能

这一阶段还是比较坑的
在实现了 editActionsForRowAtIndexPath: 代理方法之后发现并没有出现左滑删除功能,因为该手势被 UIScrollView 拦截了,只有快速左滑时才有可能出现左滑删除按钮,但这明显不符合操作习惯

然后发现将 scrollViewscrollEnabled 属性设置为 NO 时,便出现了左滑删除按钮,即:

1
self.scrollView.scrollEnabled = NO;

最初想到的解决方案是:
1、通过 touchesBegan:touchesMoved: 等方法判断触摸点出现的位置
2、如果断触摸点出现在 UITableView 区域,则设置 scrollView.scrollEnabled = NO;,否则设置 scrollView.scrollEnabled = YES;

然而实际操作之后发现,根本不会调用 touchesBegan:touchesMoved: 等方法,看来 UIScrollView 将这些方法也拦截了。
卒!
而且后来也发现即使这个方法生效也并不合理,因为对于左边的 UITableView 来说,当数据源过少,cell 并不能填满 UITableView 区域时,下半部份空白的 UITableView 区域并不能触发 UIScrollView 的滚动,因为我们并不知道 UITableView 区域是否被填满。
而我们期望的最优秀的表现应该是当对左边的 UITableView 执行左滑手势时:
1、如果 触摸点所在区域存在 cell 时,触发左滑删除功能
2、否则 触发 UIScrollView 的滚动
(当然可以根据数据源计算 cell 高度来判断,但这样就依赖了数据源并且这种根据数据计算控件高度来做判断的方法也太蠢了。。。)

所以最终的解决方案是自己实现一个 MyScrollView 继承自 UIScrollView,并重载 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer 代理方法:

1
2
3
4
5
6
7
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
if (gestureRecognizer.state != 0) {
return YES;
} else {
return NO;
}
}

这样,当 ScrollView 识别到左滑手势时仍然能向下传递该手势给它的子视图,手势识别的流程应当是:
1、ScrollView 识别到左滑手势,触发滚动功能的同时将该手势传递给它的子视图 UITableView
2、UITableView 传递该手势给它的子视图 UITableViewCell
3、如果存在子视图 UITableViewCell 则触发左滑删除功能

目前为止,已经可以实现在 UIScrollView 上嵌套左右两个 UITableView 左右滑动的功能,并且 UITableView 存在左滑删除功能,但执行左滑删除时 UIScrollView 仍在滚动,显然不合理,幸好,UIScrollView 给我们提供了如下两个方法,我们可以在左滑删除手势的开始和完成的时机设置 scrollView.scrollEnabled

1
2
3
4
5
6
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath {
self.scrollView.scrollEnabled = NO;
}
- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(nullable NSIndexPath *)indexPath {
self.scrollView.scrollEnabled = YES;
}

即 当左滑删除开始时禁止 UIScrollView 滚动,当左滑删除完成时开启 UIScrollView 的滚动。

至此便解决了 UIScrollView 左右滚动和 UITableView 左滑删除手势冲突的问题。

ScrollViewDemo GitHub 地址