理解事件的响应

事件的响应链

事件的传递和响应与响应链息息相关,下图是官网对于响应链的示例:

2018050583775chain.png

若触摸发生在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; //。表示手势是被成功后是否cancel响应链对事件的响应(touchCancel:withEvent:)。默认true
@property(nonatomic) BOOL delaysTouchesBegan; //若设置为true,在手势识别期间,被添加手势view的hit-tested view(子视图)不会响应`touchesBegan:withEvent`等事件。默认false。
@property(nonatomic) BOOL delaysTouchesEnded; //若设置为true,触摸完成时,会延迟一小段时间(0.15s)再调用响应者的`touchesEnded:withEvent:` 。默认为true

额外的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的情况与这个类似。

举例: 类似高德地图的效果。

2018050526930221cc2.gif

1). 方案1. 我们在tableView上添加pan手势来调整tableView的尺寸大小,并限定只有tableView的被拖动到顶端后才可以滑动。

重写代理方法,可以使得两个手势同时生效:

//返回true,则表示两个手势同时使用
//否则仅最新添加的生效
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,背景为透明。 理论可行。

interactivePopGestureRecognizer与UIScrollView

众所周知,重写导航栏返回按钮会使得系统自带的返回手势失效,通常的做法:

override func viewDidLoad() {
   super.viewDidLoad()        
   self.interactivePopGestureRecognizer?.delegate = self
}
    
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
   //可在此处可以对某些controller单独处理
   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

未完待续..

相关链接

iOS触摸事件全家桶
iOS 响应链-Hit-Testing
@UIAPPLICATIONMAIN

angular环境搭建,配合webstorm运行 1.安装、启动、项目结构