iOS开发之事件传递响应链

当我们在使用微信等工具,点击扫一扫,就能打开二维码扫描视图。在我们点击屏幕的时候,iphone OS获取到了用户进行了“单击”这一行为,操作系统把包含这些点击事件的信息包装成UITouch和UIEvent形式的实例,然后找到当前运行的程序,逐级寻找能够响应这个事件的对象,直到没有响应者响应。这一寻找的过程,被称作事件的响应链,如下图所示,不用的响应者以链式的方式寻找

事件响应链

一、响应者

在iOS中,能够响应事件的对象都是UIResponder的子类对象。UIResponder提供了四个用户点击的回调方法,分别对应用户点击开始、移动、点击结束以及取消点击,其中只有在程序强制退出或者来电时,取消点击事件才会调用。

UIResponder的点击事件

在自定义UIView为基类的控件时,我们可以重写这几个方法来进行点击回调。在回调中,我们可以看到方法接收两个参数,一个UITouch对象的集合,还有一个UIEvent对象。这两个参数分别代表的是点击对象和事件对象。

1、事件对象
iOS使用UIEvent表示用户交互的事件对象,在UIEvent.h文件中,我们可以看到有一个UIEventType类型的属性,这个属性表示了当前的响应事件类型。分别有多点触控、摇一摇以及远程操作(在iOS之后新增了3DTouch事件类型)。在一个用户点击事件处理过程中,UIEvent对象是唯一的
2、点击对象
UITouch表示单个点击,其类文件中存在枚举类型UITouchPhase的属性,用来表示当前点击的状态。这些状态包括点击开始、移动、停止不动、结束和取消五个状态。每次点击发生的时候,点击对象都放在一个集合中传入UIResponder的回调方法中,我们通过集合中对象获取用户点击的位置。其中通过- (CGPoint)locationInView:(nullable UIView *)view获取当前点击坐标点,- (CGPoint)previousLocationInView:(nullable UIView *)view获取上个点击位置的坐标点。
为了确认UIView确实是通过UIResponder的点击方法响应点击事件的,我创建了UIView的类别,并重写+ (void)load方法,使用method_swizzling的方式交换点击事件的实现

+ (void)load
  Method origin = class_getInstanceMethod([UIView class], @selector(touchesBegan:withEvent:));
  Method custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesBegan:withEvent:));
  method_exchangeImplementations(origin, custom);

  origin = class_getInstanceMethod([UIView class], @selector(touchesMoved:withEvent:));
  custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesMoved:withEvent:));
  method_exchangeImplementations(origin, custom);

  origin = class_getInstanceMethod([UIView class], @selector(touchesEnded:withEvent:));
  custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesEnded:withEvent:));
  method_exchangeImplementations(origin, custom);
}

- (void)lxd_touchesBegan: (NSSet *)touches withEvent: (UIEvent *)event
{
  NSLog(@"%@ --- begin", self.class);
  [self lxd_touchesBegan: touches withEvent: event];
}

- (void)lxd_touchesMoved: (NSSet *)touches withEvent: (UIEvent *)event
{
  NSLog(@"%@ --- move", self.class);
  [self lxd_touchesMoved: touches withEvent: event];
}

- (void)lxd_touchesEnded: (NSSet *)touches withEvent: (UIEvent *)event
{
  NSLog(@"%@ --- end", self.class);
  [self lxd_touchesEnded: touches withEvent: event];
}

在新建的项目中,我分别创建了AView、BView、CView和DView四个UIView的子类,然后点击任意一个位置:

项目结构图

在我点击上图绿色视图的时候,控制台输出了下面的日志(日期部分已经去除):

CView --- begin
CView --- end

由此可见在我们点击UIView的时候,是通过touches相关的点击事件进行回调处理的。

除了touches回调的几个点击事件,手势UIGestureRecognizer对象也可以附加在view上,来实现其他丰富的手势事件。在view添加单击手势之后,原来的touchesEnded方法就无效了。最开始我一直认为view添加手势之后,原有的touches系列方法全部无效。但是在测试demo中,发现view添加手势之后,touchesBegan方法是有进行回调的,但是moved跟ended就没有进行回调。因此,在系统的touches事件处理中,在touchesBegan之后,应该是存在着一个调度后续事件(nextHandler)处理的方法,个人猜测事件调度的处理大致如下图示:

事件调度

二、响应链传递

上面已经介绍了某个控件在接收到点击事件时的处理,那么系统是怎么通过用户点击的位置找到处理点击事件的view的呢?
在上文我们已经说过了系统通过不断查找下一个响应者来响应点击事件,而所有的可交互控件都是UIResponder直接或者间接的子类,那么我们是否可以在这个类的头文件中找到关键的属性呢?

正好存在着这么一个方法:- (nullable UIResponder *)nextResponder,通过方法名我们不难发现这是获取当前view的下一个响应者,那么我们重写touchesBegan方法,逐级获取下一响应者,直到没有下一个响应者位置。相关代码如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  UIResponder * next = [self nextResponder];
  NSMutableString * prefix = @"".mutableCopy;

  while (next != nil) {
    NSLog(@"%@%@", prefix, [next class]);
    [prefix appendString: @"--"];
    next = [next nextResponder];
  }
}

控制台输出的所有下级事件响应者如下:

AView
--UIView
----ViewController
------UIWindow
--------UIApplication
----------AppDelegate

虽然结果非常有层次,但是从系统逐级查找响应者的角度上来说,这个输出的顺序是刚好相反的。为什么会出现这种问题呢?我们可以看到输出中存在一个ViewController类,说明UIViewController也是UIResponder的子类。但是我们可以发现,controller是一个view的管理者,即便它是响应链的成员之一,但是按照逻辑来说,控制器不应该是系统查找对象之一,通过nextResponder方法查找的这个思路是不正确的。

后来,发现在UIView的头文件中存在这么两个方法,分别返回UIView和BOOL类型的方法:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;  // default returns YES if point is in bounds

根据方法名,一个是根据点击坐标返回事件是否发生在本视图以内,另一个方法是返回响应点击事件的对象。通过这两个方法,我们可以猜到,系统在收到点击事件的时候通过不断遍历当前视图上的子视图的这些方法,获取下一个响应的视图。因此,继续通过method_swizzling方式修改这两个方法的实现,并且测试输出如下:

UIStatusBarWindow can answer 1
UIStatusBar can answer 0
UIStatusBarForegroundView can answer 0
UIStatusBarServiceItemView can answer 0
UIStatusBarDataNetworkItemView can answer 0
UIStatusBarBatteryItemView can answer 0
UIStatusBarTimeItemView can answer 0
hit view: UIStatusBar
hit view: UIStatusBarWindow
UIWindow can answer 1
UIView can answer 1
hit view: _UILayoutGuide
hit view: _UILayoutGuide
AView can answer 1
DView can answer 0
hit view: DView
BView can answer 0
hit view: BView
hit view: AView
hit view: UIView
hit view: UIWindow
...... //下面是touches方法的输出

最上面的UIStatusBar开头的类型大家可能没见过,但是不妨碍我们猜到这是状态栏相关的一些视图,具体可以查找苹果的文档中心(Xcode中快捷键shift+command+0打开)。从输出中不难看出系统先调用pointInSide: WithEvent:判断当前视图以及这些视图的子视图是否能接收这次点击事件,然后在调用hitTest: withEvent:依次获取处理这个事件的所有视图对象,在获取所有的可处理事件对象后,开始调用这些对象的touches回调方法

通过输出的方法调用,我们可以看到响应查找的顺序是: UIStatusBar相关的视图 -> UIWindow -> UIView -> AView -> DView -> BView(系统在事件链传递的过程中一定会遍历所有的子视图判断是否能够响应点击事件),以本文demo为例,我们可以得出事件响应链查找的图示如下:

响应者查找流程

那么在上面的查找响应者流程完成之后,系统会将本次事件中的点击转换成UITouch对象,然后将这些对象和UIEvent类型的事件对象传递给touchesBegan方法,you

不仅如此,从上面输出的nextResponder来看,所有的响应者都是在查找中返回可响应点击的视图。因此,我们可以推测出UIApplication对象维护着自己的一个响应者栈,当pointInSide: withEvent:返回yes的时候,响应者入栈。

响应者栈

栈顶的响应者作为最优先处理事件的对象,假设AView不处理事件,那么出栈,移交给UIView,以此下去,直到事件得到了处理或者到达AppDelegate后依旧未响应,事件被摒弃为止。通过这个机制我们也可以看到controller是响应者栈中的例外,即便没有pointInSide: withEvent:的方法返回可响应,controller依旧能够入栈成为UIView的下一个响应者。

三、响应链应用

既然已经知道了系统是怎么获取响应视图的流程了,那么我们可以通过重写查找事件处理者的方法来实现不规则形状点击。最常见的不规则视图就是圆形视图,在demo中我设置view的宽高为200,那么重写方法事件如下:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
  const CGFloat halfWidth = 100;
  CGFloat xOffset = point.x - 100;
  CGFloat yOffset = point.y - 100;
  CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset);
  return radius <= halfWidth;
}

最终的效果图如下:

以上就是本文的全部内容,希望对大家的学习有所帮助。

(0)

相关推荐

  • iOS通过多种方式创建控制器

    本文教大家通过storyboard创建控制器,希望对大家的学习有所帮助. 1.怎么自己通过storyboard创建控制器,之前都是系统加载storyboard,帮我们创建好控制器. 通过UIStoryboard这个对象,就能加载storyboard文件 注意:必须要有storyboard,创建UIStoryboard对象才有意义,alloc init创建UIStoryboard对象没有意义 1.1> instantiateInitialViewController:默认加载箭头指向的控制器 1.

  • 详解iOS开发中使用storyboard创建导航控制器的方法

    关于StoryBoard iOS5之后Apple提供了一种全新的方式来制作UI,那就是StoryBoard.简单理解来说,可以把StoryBoard看做是一组viewController对应的xib,以及它们之间的转换方式的集合.在StoryBoard中不仅可以看到每个ViewController的布局样式,也可以明确地知道各个ViewController之间的转换关系.相对于单个的xib,其代码需求更少,也由于集合了各个xib,使得对于界面的理解和修改的速度也得到了更大提升.减少代码量就是减少

  • 深入讲解iOS开发中应用数据的存储方式

    XML属性列表-plist 一.应用沙盒 每个iOS应用都有⾃己的应⽤沙盒(应用沙盒就是文件系统目录),与其他文件系统隔离.应⽤必须待在⾃己的沙盒里,其他应用不能访问该沙盒(提示:在IOS8中已经开放访问) 应⽤沙盒的文件系统⽬录,如下图所示(假设应用的名称叫Layer) 模拟器应⽤用沙盒的根路径在: (apple是⽤用户名, 7.0是模拟器版本) /Users/apple/Library/Application Support/iPhone Simulator/7.0/Applications

  • iOS开发使用XML解析网络数据

    前言:本篇随笔介绍的是XML解析. 正文: 1.XML解析方式有2两种: DOM:一次性将整个XML数据加载进内存进行解析,比较适合解析小文件SAX:从根元素开始,按顺序一个元素一个元素往下解析,比较适合解析大文件 2.IOS中XML解析方案有很多种: 2-1.第三方框架: libxml2:纯C语言,默认包含在iOS SDK中,同时支持DOM和SAX解析 GDataXML:DOM方式解析,由Google开发,基于libxml2 2-2.苹果原生 NSXMLParser:SAX方式解析,使用简单

  • 详解iOS开发中UItableview控件的数据刷新功能的实现

    实现UItableview控件数据刷新 一.项目文件结构和plist文件 二.实现效果 1.说明:这是一个英雄展示界面,点击选中行,可以修改改行英雄的名称(完成数据刷新的操作). 运行界面: 点击选中行: 修改数据后自动刷新: 三.代码示例 数据模型部分: YYheros.h文件 复制代码 代码如下: // //  YYheros.h //  10-英雄展示(数据刷新) // //  Created by apple on 14-5-29. //  Copyright (c) 2014年 itc

  • iOS开发使用JSON解析网络数据

    前言:对服务器请求之后,返回给客户端的数据,一般都是JSON格式或者XML格式(文件下载除外) 本篇随便先讲解JSON解析. 正文: 关于JSON: JSON是一种轻量级的数据格式,一般用于数据交互JSON的格式很像Objective-C中的字典和数组:{"name":"jack","age":10} 补充: 标准的JSON格式的注意点:key必须用双引号.(但是在Java中是单引号) JSON-OC的转换对照表 其中:null--返回OC里的N

  • iOS应用开发中UITabBarController标签栏控制器使用进阶

    做了这么长时间的ios开发了,最基本的UITabBarController和UINavigationController都用了好长时间了,总是改现成的代码,或者各种自定义控件的修改,用的都有些混乱了,呵呵.还是自己做个demo再复习一下吧,记录下来以备后续翻查. 一.UITabBarController和UINavigationController的联合使用 这种方法最常见,好像一般有tabbar都会有naviBar.一般使用, 1. 在appDelegate里面创建UITabBarContro

  • iOS开发中导航控制器的基本使用教程

    多控制器和导航控制器简单介绍 一.多控制器 一个iOS的app很少只由一个控制器组成,除非这个app极其简单.当app中有多个控制器的时候,我们就需要对这些控制器进行管理 有多个view时,可以用一个大的view去管理1个或者多个小view,控制器也是如此,用1个控制器去管理其他多个控制器 比如,用一个控制器A去管理3个控制器B.C.D.控制器A被称为控制器B.C.D的"父控制器":控制器B.C.D的被称为控制器A的"子控制器" 为了便于管理控制器,iOS提供了2个

  • 详解iOS的UI开发中控制器的创建方法

    控制器的创建 说明:控制器有三种创建方式,下面一一进行说明. 一.第一种创建方式(使用代码直接创建) 1.创建一个空的IOS项目. 2.为项目添加一个控制器类. 3.直接在代理方法中创建一个控制器. 复制代码 代码如下: #import "YYAppDelegate.h" #import "YYViewController.h" @implementation YYAppDelegate - (BOOL)application:(UIApplication *)ap

  • iOS实现两个控制器之间数据的双向传递

    本文为大家分享了iOS控制器之间数据的双向传递,供大家参考,具体内容如下 首先,有两个控制器,分别为控制器A.控制器B. A->B:数据由控制器A传向控制器B,这叫做数据的顺传:数据由控制器B传向控制器A,这叫做逆传. 顺传:一般通过创建目标控制器对象,将数据赋值给对象的成员来完成: 逆传:一般使用代理来实现,其中控制器A是控制器B的代理(控制器A监听控制器B,控制器B通知控制器A). 下面是博主写的简单实现了两个控制间实现数据的双向传递的app的demo: 1.这是界面设计: FirstVie

随机推荐