iOS事件机制

事件分类

  • Touch events(触摸事件,经常用)
  • motion events(运动事件,比如重力感应和摇一摇等)
  • remote control event(远程控制事件,比如用耳机上得按键来控制手机)
  • press event(按压事件)

事件的产生

当我们用手指点击iOS设备屏幕时,屏幕中的硬件侦测到触摸的位置等信息。这些信息被用来创建一个touch event对象,这个对象记录了触摸的位置信息、触摸发生的时间、触摸持续时间等信息。iOS将touch event对象放入到app中的事件队列中。

事件传递(Event Delivery)

事件传递是指一个事件从事件队列中取出并传递到应用程序中相应对象的过程。

事件传递有三种方式:

  • Direct delivery
  • Hit-Test
  • First responder

Direct delivery(直接传递)

直接传递是最简单的传递方式。某些事件会触发特定的对象。这些事件知道谁能够处理自己,并且它们是被runloop直接分派处理的,比如Swift函数调用。其他能触发特定对象(或一组对象)事件是:通知(notifications)、定时器(timer events)、用户界面更新。

Hit-Test

Hit-Test传递事件基于用户界面视图的层次关系,并且它仅用于触摸事件。Hit-Test的目的是找到触摸事件发生的且位于最上层的视图,该视图被称为Hit-Test View.
以下图为例:
img

  • UIApplication对象给KeyWindow发送sendEvent(_:)消息,参数是event对象
  • UIWindow对象执行Hit-Test. 从视图层次的最上层开始,对子视图发送hitTest(_:,withEvent:)消息,如上图第二个图所示,即控制器的view.
  • 上层的子视图判断触摸点是否在自己的bounds中,如果在,则它会继续执行Hit-Test,这是一个递归的过程.图中view包含三个部分:顶部的导航条,中间的webview,底部的工具条。触摸点在工具条的范围内,所以会向工具条发送hitTest(_:,withEvent:)消息。工具条继续重复这个过程,寻找包含触摸点的子视图,最后发现左侧的button包含这个事件的触摸点并且button没有子视图,于是,这个button就被返回作为Hit-Test View
  • 最后,这个button处理该事件,判断是否点击该按钮,然后给target发送action消息。

Hit-Test整个过程的示意图:
img

Hit-Test过程中用到的是UIView的方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; 
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

Hit-Test过程伪代码:

//override
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subView in [[self subviews] reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subView convertPoint:point fromView:self];
            UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

First responder(第一响应者)

第一响应者是在激活的用户界面中的视图(UITextView, UITextField等)、控制器或UIWindow对象。可以把它作为不能通过hit-Test决定的指定事件的接收者。每一个激活的用户界面都有一个第一响应者。

下面的事件会被传递到第一响应者:

  • Shake motion events(通过硬件加速器产生)
  • Remote control events(通过外部附件产生,如耳机)
  • Key events(通过点击虚拟键盘或者蓝牙连接的键盘产生)

UITextField和UITextView被设计为第一响应者,canBecomeFirstResponder()被重写返回YES.在控制器的viewDidAppear()方法中显示调用becomeFirstResponder()方法,可使UITextField或UITextView对象成为第一响应者。

响应者链(The Responder Chain)

响应者链由一系列UIResponder子类对象构成。

如下图所示:响应者链始于initial responder(初始化响应者)。当传递motion, key, 和remote events时, 第一响应者是initial responder;传递触摸事件时, Hit-Test View是initial responder.
img

响应者链的构建

响应者链的构建是自动完成的。

在UIKit中有一个类:UIResponder,我们可以看看头文件的几个属性和方法:

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL canBecomeFirstResponder;    // default is NO
#else
- (BOOL)canBecomeFirstResponder;    // default is NO
#endif
- (BOOL)becomeFirstResponder;

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL canResignFirstResponder;    // default is YES
#else
- (BOOL)canResignFirstResponder;    // default is YES
#endif
- (BOOL)resignFirstResponder;

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL isFirstResponder;
#else
- (BOOL)isFirstResponder;
#endif

// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (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;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

UIResponder是所有可以响应事件的类的基类,其中包括最常见的UIView和UIViewController甚至是UIApplication。

我们的app中,所有的视图都是按照一定的结构组织起来的,即树状层次结构,每个view都有自己的superView,包括controller的topmost view(controller的self.view)。当一个view被add到superView上的时候,它的nextResponder属性就会被指向它的superView,当controller被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller,而controller的nextResponder会被指向self.view的superView,这样,整个app就通过nextResponder串成了一条链,也就是我们所说的响应链。所以响应链就是一条虚拟的链,并没有一个对象来专门存储这样的一条链,而是通过UIResponder的属性串连起来的。

事件处理(Event Handling)

有了响应链,并且找到了第一个响应事件的对象,接下来就是把事件发送个这个响应者了。 UIApplication中有个sendEvent:的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的接口,把事件发送给initial responder。而事件的响应,也就是按钮上绑定的action,是在touchEnded里面通过调用UIApplication的sendAction:to:from:forEvent:方法来实现的。如果initial responder没有响应这个事件,那么就会根据响应链,把事件冒泡传递给nextResponder来响应。

目标对象怎样才能处理事件呢?答案就是实现处理事件的方法,处理事件时给出处理方法,不处理时调用super方法,事件会沿着响应者链向上传递。

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
    if iWantToHandleTheseTouches(touches) {
        // handle event
        doSomethingWithTheseTouches(touches)
    } else {
        // ignore event and pass it up the responder chain
        super.touchesBegan(touches, withEvent: event)
    }
}

http://zhoon.github.io/ios/2015/04/12/ios-event.html