文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. 结论
  4. 4. 分析
  5. 5. 解决出现的UI小问题

文档更新说明

  • 最后更新 2020年03月05日
  • 首次更新 2020年03月06日

前言

  昨天有个同事发烧了, 然后整层楼的人都不能去上班了, 只能待在家里, 所以我寻思着最近做的东西就写了这篇文章了.

这个星期刚写的一个类似Excel的SheetView, 其中涉及到点击冲突问题, 当时查了一下网上的解决方案, 大概就是分成两种做法:

一种是重写TableViewCellHitTest方法, 屏蔽掉CollectionView的事件响应, 这个就和把CollectionView设置成userInteractionEnabled=NO一样粗鲁了, 这样CollectionView除了不能点击之外, 也完全没法响应滚动手势了.

另一种就是监听CollectionViewdidSelectItemAtIndexPath方法, 或者在每个CollectionViewCell上都加一个点击手势, 用户点击的时候就把事件传给TableView, 这方案也是没有办法的办法了.

经过一段时间思考后, 我尝试找出了一个比较好的解决方案, 也就是今天这篇文章要说的.

结论

  分析部分会比较长, 先上结论.

创建一个CollectionView的子类, CCPenetrateCollectionView, 重写四个touches方法, 把触摸事件手动抛给nextResponder, 这样就可以让TableView的didSelectRowAtIndexPath得到响应了. 说明一下, 直接向nextResponder发送触摸信息是不被苹果推荐的, 所以后面分析的时候会提到如何处理一些意料之外的问题.

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self.nextResponder touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[self.nextResponder touchesEnded:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
[self.nextResponder touchesMoved:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
[self.nextResponder touchesCancelled:touches withEvent:event];
}

分析

  UICollectionView类继承自UIScrollView, 这两个类都有同一个特点, 均会拦截触摸事件, 不管UICollectionViewallowsSelection是否为NO, 结果都是一样的. 这就是为什么在TableViewCell上嵌套一个UICollectionView, 尽管UICollectionView本身已经被设置为allowsSelection = NO, 但是TableView的Cell还是不能被选中的原因.
  
说到事件的传递, 简单复习一下. 我试试简单描述这个过程. 假设用户用一个手指触摸了屏幕, 这前后App内部的处理流程是这样的:

  1. UIApplicationMain函数维护的RunLoop(一个死循环), 正处于等待唤醒的状态.
  2. 用户触摸屏幕那一刻, RunLoop中会收到系统底层发来的唤醒信号, RunLoop从唤醒处开始执行下一行代码. (以下都称为UIApplicationMain的操作)
  3. UIApplicationMain开始查找屏幕上能响应事件的UIWindow, 正常我们的App就一个UIWindow ,并开始调用Window上的根View的hitTest:withEvent:方法.
  4. hitTest:withEvent:内部则会调用视图自身的pointInside:withEvent:, 确定当前这个触摸点是不是在自己的View范围内, 如果是则继续; 否则直接返回nil, 并且响应结束, 什么事都没有发生.
  5. pointInside:withEvent:返回YES之后, hitTest:withEvent:方法内部会继续遍历它的子View, 重复4,5这两个步骤, 直到叶子View后, 会得到一个被点击的最上层的View, 并且一路返回给UIApplicationMain
  6. UIApplicationMain得到了符合响应条件的View之后, 开始调用这个View所在的UIApplicationsendEvent:方法, UIApplication继续调用UIWindowsendEvent:, 继续逐步调用, 最后View的touchesBegan:withEvent:被调用了(后续还有另外三个touches方法也是一样道理), 视图开始处理事件.
  7. View处理完成事件之后, 会根据自身的视图类型来决定是否把这个事件传递给下一个响应者, 也就是View的nextResponder对象(UIView默认实现是会传给nextResponder), 一般这个nextResponder就是View的父视图, 如果是Controller管理的View, 则nextResponder会是对应的Controller. 一直把事件传到最后一个响应者就结束了.

上面就是整个事件响应的过程了, 我是从程序调用栈中看出来的, 结合一下网上的资料总结出的, 我感觉应该是够用了. 其中第7步, 很像是web中说的事件冒泡.

通过分析, 应该可以很清楚知道怎么解决CollectionView没有把点击事件传给TableViewCell的问题了, 解决的代码就是上面那样了. 由于不知道CollectionView内部的touches…方法是怎么写的, 直接把事件传给TableViewCell会引发一些UI的小问题.

解决出现的UI小问题

  第一个遇到的问题, 就是TableViewCell如果支持高亮的话, 可能会因为手指在CollectionView上长按后松开导致TableViewCell一直是高亮状态, 这个可以通过下面代码解决
  

/// 重写TableView子类
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
[self setHighlighted:NO animated:YES];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
[self setHighlighted:NO animated:YES];
}

第二个遇到的问题, 如果长按某一行, 之后松开点击其他行, 这个时候会触发被长按的那行的点击事件, 这个就是苹果不推荐你直接调用nextResponder的touches方法的原因的😂, 开发者不知道内部实现, 不能随便乱用, 否则会出现这样的意料之外的事. 这个问题暂时找不到怎么解决, 算是一种遗憾.

文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. 结论
  4. 4. 分析
  5. 5. 解决出现的UI小问题