理解事件的响应
事件的响应链
事件的传递和响应与响应链息息相关,下图是官网对于响应链的示例:
若触摸发生在 UITextField 上,则事件的传递顺序会沿着箭头传递到UIApplicationDelegation
,此时 textField 就是 firstResponder。
UIResponder: 响应链的节点就是响应者(UIResponder),UIView、UIViewController、UIApplication、AppDelegate 都继承自 UIResponder。
hit-test view: 当系统检测到手指触摸屏幕时,会把触摸事件加到UIApplication
的事件队列中,UIApplication 从事件队列中取出最新的触摸事件进行分发传递到 UIWindow 进行处理。UIWindow 会通过 hitTest:withEvent:方法寻找触碰点所在的视图,这个过程称之为hit-test view。
hitTest 用来确定响应者(UIResponder)。可以简单理解为获取到响应区域的最顶层(最后添加)的可以响应的视图。hitTest 方法忽略以下的视图:
- 视图是隐藏的 hidden = YES
- 用户交互关闭的 userInteractionEnabled = NO
- 透明度小于 0.01 的 alpha < 0.01
hitTest 获取 responder 顺序为: UIApplication -> UIWindow -> Root View ->..-> view1 -> subview1。 responder 可以通过subview.nextResponder
属性获取上一级响应者,比如在这里 subview1.nextResponder 为 view1。
UIResponder
响应者响应事件(不提 motion)离不开如下代理方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
与响应链相关的方法
- isFirstResponder
- nextResponder
- canBecomeFirstResponder
- becomeFirstResponder
- canResignFirstResponder
- resignFirstResponder
消息的响应和拦截是通过touchesBegan:withEvent:
方法控制的,该方法默认将沿着响应链依次调用(View—> controller 的方向)。
响应者与手势识别:
关于手势是如何是被识别的:
1.肯定会调用touchBegin:withEvent:
2.手势被识别后,Application 会取消该视图对事件的响应 ,会调用touchCancel:withEvent:
方法
手势识别器的 3 个属性
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;
额外的 scrollView 的几个属性:
delaycontenttouch
: 用于确定滚动视图是否延迟触摸手势的处理。如果 true,则会延迟处理向下的触摸手势,直到确定该视图是要滚动了;如果 false,则滚动视图会立即调用touchesShouldBegin(_:with:in :)
。
touchesShouldCancelInContentView:
方法,可以重写方法处理子视图是否需要 cancel。
override func touchesShouldCancel(in view: UIView) -> Bool {
if view is UIButton {
return true
}
return super.touchesShouldCancel(in: view)
}
main 函数与 UIApplication
main 函数,UIApplicationMain
第三个参数可以传递 UIApplication 对象, nil 为默认的对象。根据需求可以自定义一个 application 对象,在方法里对事件进行拦截。
int main(int argc, char * argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, NSStringFromClass([MyApplication class]),
NSStringFromClass([AppDelegate class]));
}
}
swift 可以去掉@UIApplicationMain
,再新建一个 main.swift 文件,方法如下
import UIKit
class MyApplication: UIApplication {
override func sendEvent(_ event: UIEvent) {
print(event)
super.sendEvent(event)
}
}
UIApplicationMain(
CommandLine.argc,
UnsafeMutableRawPointer(CommandLine.unsafeArgv)
.bindMemory(
to: UnsafeMutablePointer<Int8>.self,
capacity: Int(CommandLine.argc)),
NSStringFromClass(MyApplication.self),
NSStringFromClass(AppDelegate.self)
)
滚动视图添加滑动手势
scollView 上添加滑动手势,会与 scollView 本身自带的手势冲突,导致默认只响应后添加的手势。
scrollView 上添加 tableView 的情况与这个类似。
举例: 类似高德地图的效果。
1). 方案 1. 我们在 tableView 上添加 pan 手势来调整 tableView 的尺寸大小,并限定只有 tableView 的被拖动到顶端后才可以滑动。
重写代理方法,可以使得两个手势同时生效:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
然后根据是否拉到顶端判断是否可以滑动 tableView, 不能滑动 tableView 的时候tableView.isScrollEnabled = false
缺点: tableView 滑动到顶端需要重新滑动才能滑动 cell.
2). 方案 2. 不重写gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
方法。只根据滑动手势来调整 tableView 的 frame 和 contentOffset。
缺点: 卡顿,效果不够流畅。
3). 方案 3 (最终方案). 重写gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
方法,返回 true; 通过 tableView 的代理方法控制 tableView 偏移;不能滚动的时候使tableView = false
。
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if canScroll == false {
scrollView.contentOffset = CGPoint.zero
}
if scrollView.contentOffset.y <= 0 {
canScroll = false
}
}
为了防止 cell 点击延迟,设置下 delaycontenttouch=false 即可.
关键代码: Gist
4). 其他方案。 只调整视图的 contentInset,背景为透明。 理论可行。
众所周知,重写导航栏返回按钮会使得系统自带的返回手势失效,通常的做法:
override func viewDidLoad() {
super.viewDidLoad()
self.interactivePopGestureRecognizer?.delegate = self
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return self.childViewControllers.count == 0 ? false : true
}
但是可能与控制器的 scollView 滑动冲突,处理方法:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if self.interactivePopGestureRecognizer == gestureRecognizer {
if let scrollView = otherGestureRecognizer.view as? UIScrollView {
if ((scrollView.contentSize.width > self.view.bounds.size.width && scrollView.contentOffset.x == 0)) {
return true;
}
}
}
return false
}
参考自[interactivePopGestureRecognizer interferes with UIScrollView](interactivePopGestureRecognizer interferes with UIScrollView)
未完待续..
相关链接
iOS 触摸事件全家桶
> iOS 响应链-Hit-Testing
> @UIAPPLICATIONMAIN