写在前面

最近的一个iOS App项目中遇到了这么问题:通过App访问服务器的大多数资源不需要登录,但是访问某些资源是需要用户提供验证的,一般来说,通常App的做法(譬如美团App)将这些资源放在“我的”模块,在未登录情况下,当点击“我的”某个模块时,以modally给出一个界面用于登录,我不晓得美团是怎么实现的;但在我看来,比较优雅的方式是在网络底层实现“modally呈现登录界面”,具体来说,当用户欲访问“我的购物券”时,在网络访问层发现用户未登录,就弹出一个登陆界面,用户登录成功后,就将这个登录界面给dismiss掉。

这种实现的好处是使得登录模块管理比较集中,便于维护。其实现也并不难,modally方式呈现一个页面的代码很简单,如下:

1
2
UIViewController *VC = [][UIViewController alloc] init];
[self presentViewController:VC animated:YES completion:nil];

这对于在网络层处理有点麻烦,因为presentViewController:animated:completion:是UIViewController中定义的方法,所以,modally呈现一个页面的第一个任务是需要知道当前所呈现的view controller。搜索“iOS获取当前view controller”在这里貌似找到一个比较好的解决方案。所以最后我的关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
UIViewController *result = nil;
 
// 1. get current window
UIWindow * window = [[UIApplication sharedApplication] keyWindow];
if (window.windowLevel != UIWindowLevelNormal) {
NSArray *windows = [[UIApplication sharedApplication] windows];
for(UIWindow * tmpWin in windows) {
if (tmpWin.windowLevel == UIWindowLevelNormal) {
window = tmpWin;
break;
}
}
}
 
// 2. get current View Controller
UIView *frontView = [[window subviews] objectAtIndex:0];
id nextResponder = [frontView nextResponder];
 
if ([nextResponder isKindOfClass:[UIViewController class]]) {
result = nextResponder;
} else {
result = window.rootViewController;
}
 
// 3. present Login VC modally
// 3.1 init VC
LoginNavigationController *nav = [[LoginNavigationController alloc] init];
 
// 3.2 show VC
[result presentViewController:nav animated:YES completion:nil];

从未对这段代码进行验证,今天得空决定对这段简单的代码进行简单的分析。之前对UIResponder并不熟悉,得补充补充相关知识点,笔者参考的文档是官方文档《Event Handling Guide for iOS》。

在iOS中,把“对事件的处理”称为responder。

Hit-Testing

iOS的views之间有各种覆盖关系,譬如view A是view B上的subview,那么如果在view A上产生了一个touch event,那么说“touch event在A上发生”还是“touch event在B上发生”呢?Hit-Testing就是为了解决这个问题而生的概念。

关于Hit-testing,官方文档是这么描述的:

iOS uses hit-testing to find the view that is under a touch. Hit-testing involves checking whether a touch is within the bounds of any relevant view objects. If it is, it recursively checks all of what view’s subviews. The lowest view in the view hierarchy that contains the touch point becomes the hit-test view. After iOS determines the hit-test view, it passes the touch event to that view for handling.

来用图文对此过程进行描述,如下图:

假设在view E上发生了一个user touch event,iOS的hit-test过程如下:

  1. touch event发生在view A的bounds内,继续检查subview B和subview C;
  2. touch event没有发生在view B的bounds内,但发生在view C的bounds内,检查view C的subview D和subview E;
  3. touch event没有发生在view D的bounds内,但发生在view E的bounds内,而view E之下再无subviews,所以确定view E是所谓的hit-test view。

回到上述的引文,笔者阅读这段文字时产生了一个疑问:如果view A确实是view B的subview,但是view A所对应的区域不在view B的bounds之内的,譬如下图,那么在view A上发生的touch event还能被检测到吗?

P.S:经过笔者的测验,得到的结果是不能!

在viewDidLoad方法中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
self.view.backgroundColor = [UIColor whiteColor];
 
UIView *lightGrayView = ({
UIView *view = [[UIView alloc] init];
view.backgroundColor = UIColor.lightGrayColor;
view;
});
 
[self.view addSubview:lightGrayView];
 
[lightGrayView makeConstraints:^(MASConstraintMaker *make) {
UIView *superView = self.view;
int padding = 10.0;
 
make.left.equalTo(superView.left).offset(padding);
make.right.equalTo(superView.right).offset(-padding);
make.height.equalTo(lightGrayView.width).offset(-padding);
make.centerY.equalTo(superView.centerY).offset(0);
}];
 
UIView *yellowView = ({
UIView *view = [[UIView alloc] init];
view.backgroundColor = UIColor.yellowColor;
view;
});
 
[lightGrayView addSubview:yellowView];
 
[yellowView makeConstraints:^(MASConstraintMaker *make) {
UIView *superView = lightGrayView;
 
make.width.equalTo(superView.width).multipliedBy(0.5).offset(0.0);
make.height.equalTo(yellowView.width).offset(0.0);
make.left.equalTo(superView.left).offset(0);
make.top.equalTo(superView.top).offset(-80);
}];
yellowView.userInteractionEnabled = YES;
[yellowView addGestureRecognizer:({
UITapGestureRecognizer *gesture =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(sayHello:)];
gesture.numberOfTapsRequired = 1;
gesture;
})];

P.S:代码中使用了第三方autolayout框架Masonry,结果如下图:

黄色view是灰色view的subview,但是黄色view并不全部包括在灰色view的bounds中,为黄色view添加一个tap gesture事件绑定,测试发现,当点击黄色view上半部分(其灰色bounds区域之外部分),tap event并未被触发,但是点击黄色view的下半部分,touch event会被触发。

故而,可以得出结论,如果view A是view B的subview,但view A并未在view B的bounds之内,则当view A的touch event发生在view B的bounds之外时,其响应其实并未发生(或说并不被承认)。

其实文档中也有相应的解释:

If the point passed into hitTest:withEvent: is not inside the bounds of the view, the first call to the pointInside:withEvent: method returns NO, the point is ignored, and hitTest:withEvent: returns nil. If a subview returns NO, that whole branch of the view hierarchy is ignored, because if the touch did not occur in that subview, it also did not occur in any of that subview’s subviews.This means that any point in a subview that is outside of its superview can’t receive touch events because the touch point has to be within the bounds of the superview and the subview.

找到Hit-test view又如何呢?hit-test view是第一个有机会对touch event进行处理的view;如果它不处理,则往上层走……这涉及responder chain这个概念。

The Responder Chain

responder chain在iOS中是一个非常重要的概念(相比在Android中也一样)。在了解responder chain这个概念之前,得先明白responder。文档如是说:

A responder object is an object that can responder to and handle events. The UIResponder class is the base class for all responder objects, and it defines the programmatic interface not only for event handling but also for common responder behavior.

UIApplication, UIViewController以及UIView这几个类的实例及其子类实例都是responders。

当iOS的hit-testing找到了发生的target view之后,event就在responder chain之间传送,第一个有机会处理event的responder当然是hit-testing view,如果它不能处理这个event,则会往上层(譬如super view)继续传送,但考虑到UIViewController和UIApplication的实例也是responder,所以可能传送路线不只是hit-testing view — super view — super view — super view — …那么简单,具体来说有两个的传递路线:

![QQ20150215-2]QQ20150215-2.png

为什么是两种呢?其实稍微看一下也很简单,第二种对应的是view controller嵌套的情况。

OK,回过头来看“获取当前view controller”的代码,第一个部分“get current window”没啥问题,第二部分代码(如下)就值得玩味了:

1
2
3
4
5
6
7
8
UIView *frontView = [[window subviews] objectAtIndex:0];
id nextResponder = [frontView nextResponder];
 
if ([nextResponder isKindOfClass:[UIViewController class]]) {
result = nextResponder;
} else {
result = window.rootViewController;
}

这段代码一般情况是没问题的,但根据不是特别好,因为这段代码执行的前提是key windows的subview只有一个,而不含其他的view,所以笔者稍作了一点修改:

1
2
3
4
5
6
7
8
9
10
11
// 2. get current View Controller
NSArray *subViews = [window subviews];
for (UIView *view in subViews) {
id nextResponder = [view nextResponder];
if ([nextResponder isKindOfClass:[UIViewController class]]) {
result = nextResponder;
}
}
if (result == nil) {
result = window.rootViewController;
}

OK,game over!关于responder,还有更多更“高端”的知识点,碰到了类似问题之后再说吧。

参考:
《Event Handling Guide for iOS》

理解iOS Event Handling的更多相关文章

  1. Event Handling Guide for iOS--(一)--About Events in iOS

    About Events in iOS Users manipulate their iOS devices in a number of ways, such as touching the scr ...

  2. Event Handling Guide for iOS(五)

    基本概念: 加速计: 又称加速度计,测量设备运动的加速度. 加速度: 矢量,描绘速度的方向和大小变化的快慢. 陀螺仪: 感测与维持方向的装置. 原文: Motion Event声明: 由于本人水平有限 ...

  3. Event Handling Guide for iOS--(三)---Event Delivery: The Responder Chain

    Event Delivery: The Responder Chain 事件传递:响应链 When you design your app, it’s likely that you want to ...

  4. Event Handling Guide for iOS--(二)---Gesture Recognizers

    Gesture Recognizers 手势识别器 Gesture recognizers convert low-level event handling code into higher-leve ...

  5. Event Handling in Spring

    Spring内置的event有 1.ContextRefreshedEvent This event is published when the ApplicationContext is eithe ...

  6. 0112.1——iOS开发之理解iOS中的MVC设计模式

    模型-视图-控制器(Model-View-Controller,MVC)是Xerox PARC在20世纪80年代为编程语言Smalltalk-80发明的一种软件设计模式,至今已广泛应用于用户交互应用程 ...

  7. iOS开发之理解iOS中的MVC设计模式

    模型-视图-控制器(Model-View-Controller,MVC)是Xerox PARC在20世纪80年代为编程语言Smalltalk-80发明的一种软件设计模式,至今已广泛应用于用户交互应用程 ...

  8. 我所理解的event loop

    灵魂三问 JS为什么是单线程的 我们都知道,JS是单线程的语言,那为什么呢?我的理解是JS设计之初就是为了在浏览器端完成DOM操作和一些简单交互的,既然涉及到DOM操作如果是多线程就会带来复杂的同步问 ...

  9. 通过实现一个TableView来理解iOS UI编程

    推荐一篇神作: 通过实现一个TableView来理解iOS UI编程 http://blog.jobbole.com/61101/

随机推荐

  1. 2-sat问题,输出方案,几种方法(赵爽的论文染色解法+其完全改进版)浅析 / POJ3683

    本文原创于  2014-02-12 09:26. 今复习之用,有新体会,故重新编辑. 2014-02-12 09:26: 2-sat之第二斩!昨天看了半天论文(赵爽的和俉昱的),终于看明白了!好激动有 ...

  2. 带你学Node系列之express-CRUD

    前言 hello,小伙伴们,我是你们的pubdreamcc,本篇博文出至于我的GitHub仓库node学习教程资料,欢迎小伙伴们点赞和star,你们的点赞是我持续更新的动力. GitHub仓库地址:n ...

  3. Opengl配置

    Opengl配置说明: 本配置文档针对windows64位操作系统,配置vs2008项目工程 1.下载OpenGL的glut类库:http://www.opengl.org/resources/lib ...

  4. Spring自带mock测试Controller

    原文:http://blog.csdn.net/yin_jw/article/details/24726941 准备SpringMVC环境 注意:使用mock测试需要引入spring-test包 Ba ...

  5. 通过Python实现自动填写调查问卷

    0X00 前言 快开学了,看到空间里面各种求填写调查问卷的,我才想起来貌似我也还没做.对于这种无意义的问卷,我是不怎么感冒的,所以我打算使用”特技”来完成,也就是python,顺便重新复习一下pyth ...

  6. 查询公司外网ip方法

    curl -s "http://checkip.dyndns.org/"|cut -f 6 -d" "|cut -f 1 -d"<" ...

  7. cocos2dx3.0 2048多功能版

    1.2048项目描写叙述 1.1.2048功能描写叙述 实现手机上2048的功能,同一时候具备能够删除随意一个方块的功能,悔棋功能,退出自己主动保存,启动自己主动载入功能. 1.2.2048所需技术 ...

  8. docker 搭建linux samba

    https://hub.docker.com/r/jenserat/samba-publicshare/ 需要共享目录 只需直接 挂载容器映射即可

  9. java开始到熟悉70-71

    本次内容:file类 package array; /** * file类 */ import java.io.File; import java.io.IOException; public cla ...

  10. 整理对Spark SQL的理解

    Catalyst Catalyst是与Spark解耦的一个独立库,是一个impl-free的运行计划的生成和优化框架. 眼下与Spark Core还是耦合的.对此user邮件组里有人对此提出疑问,见m ...