写在前面

最近的一个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. DispatcherServlet url-pattern中 /、/*、*.do中的区别与作用

    DispatcherServlet url-pattern中 /./*.*.do中的区别与作用 "/'表示匹配所有请求(其中包含除.jsp和.jspx外的所有后缀). 如果不配置静态资源,它 ...

  2. Linux Shell高级技巧

    Linux Shell高级技巧(一) http://www.cnblogs.com/stephen-liu74/archive/2011/12/22/2271167.html一.将输入信息转换为大写字 ...

  3. 谷歌訪问之直接输入ip地址

    废话啥说.直接上IP: 173.194.121.51 173.194.43.19 173.194.65.147 74.125.235.148

  4. SODBASE CEP学习(四)续:类SQL语言EPL与Storm或jStorm集成-使用分布式缓存

    流式计算在一些情况下会用到分布式缓存,从而实现(1)想把统计或计算结果保存在分布缓存中.供其他模块或其他系统调用. (2)某一滑动时间窗体上计数.比如实时统计1小时每一个Cookie的訪问量.实时统计 ...

  5. weexapp 开发流程(三)其他页面创建

    1.首页 (1)轮播图 步骤一:创建 轮播图 组件(Slider.vue) src / assets / components / Slider.vue <!-- 轮播图 组件 --> & ...

  6. 二:redis 的hash类型相关操作

    =====================二种:hash类型================== 介绍:redis -> hash是一个string类型的field和value的映射表 hash ...

  7. Apatch常用的commons工具包介绍

    1.Commons BeanUtils http://jakarta.apache.org/commons/beanutils/index.html 说明:针对Bean的一个工具集.由于Bean往往是 ...

  8. 加载和执行 --《高性能JavaScript》

    1.起因: 每次遇到<script> 标签时,页面必须停下来等待代码下载并执行完,然后再继续处理其他部分. 2.减少JavaScript对性能的影响 1.将所有的JavaScript文件放 ...

  9. ubuntu搭建samba服务器

    一.为什么要用Samba?     Samba的主要任务就是实现Linux系统和Windows系统之间的资源共享.   二.需要的软件? 我是在ubuntu上实现的,所以我只需在配置好ubuntu的更 ...

  10. 怎样搭建svn本地server,管理本地的代码

    搭建svn本地server,以下是详细的步骤介绍. 一.准备工作 1.下载svnserver端:Subversion. 到官方站点(http://s version.tigris.org/)下载最新的 ...