理解iOS Event Handling
写在前面
最近的一个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过程如下:
- touch event发生在view A的bounds内,继续检查subview B和subview C;
- touch event没有发生在view B的bounds内,但发生在view C的bounds内,检查view C的subview D和subview E;
- 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的更多相关文章
- 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 ...
- Event Handling Guide for iOS(五)
基本概念: 加速计: 又称加速度计,测量设备运动的加速度. 加速度: 矢量,描绘速度的方向和大小变化的快慢. 陀螺仪: 感测与维持方向的装置. 原文: Motion Event声明: 由于本人水平有限 ...
- 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 ...
- Event Handling Guide for iOS--(二)---Gesture Recognizers
Gesture Recognizers 手势识别器 Gesture recognizers convert low-level event handling code into higher-leve ...
- Event Handling in Spring
Spring内置的event有 1.ContextRefreshedEvent This event is published when the ApplicationContext is eithe ...
- 0112.1——iOS开发之理解iOS中的MVC设计模式
模型-视图-控制器(Model-View-Controller,MVC)是Xerox PARC在20世纪80年代为编程语言Smalltalk-80发明的一种软件设计模式,至今已广泛应用于用户交互应用程 ...
- iOS开发之理解iOS中的MVC设计模式
模型-视图-控制器(Model-View-Controller,MVC)是Xerox PARC在20世纪80年代为编程语言Smalltalk-80发明的一种软件设计模式,至今已广泛应用于用户交互应用程 ...
- 我所理解的event loop
灵魂三问 JS为什么是单线程的 我们都知道,JS是单线程的语言,那为什么呢?我的理解是JS设计之初就是为了在浏览器端完成DOM操作和一些简单交互的,既然涉及到DOM操作如果是多线程就会带来复杂的同步问 ...
- 通过实现一个TableView来理解iOS UI编程
推荐一篇神作: 通过实现一个TableView来理解iOS UI编程 http://blog.jobbole.com/61101/
随机推荐
- LightOJ1234 Harmonic Number 调和级数求和
[题目] [预备知识] ,其中r是欧拉常数,const double r= 0.57721566490153286060651209; 这个等式在n很大 的时候 比较精确. [解法]可以在 n较小的时 ...
- mysql 5.7版本目录无data文件夹的解决办法
安装mysql 5.7+版本时,若发现因根目录下,缺少data文件夹的情况, ***请不要去拷贝其他版本的data文件夹!*** 因为此操作会出现很多潜在问题:比如我遇到的执行show variabl ...
- jquery 禁用/启用滚动条
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 50 个 Bootstrap 插件
Bootstrap是快速开发Web应用程序的前端工具包.它是一个CSS和HTML的集合,它使用了最新的浏览器技术,给你的Web开发提供了时尚的版式,表单,buttons,表格,网格系统等等. 本文向你 ...
- flask如何处理并发
1.使用自身服务器的多进程或者多线程,参考werkzeug的run_simple函数的入参.注意,进程和线程不能同时开启 2.使用gunicorn使用多进程,-w worker 进程数,类型于运行多个 ...
- windows redis 服务安装坑
环境 winserver 2012 最新版的redis:3.0.503 redis-server.exe --service-install redis.windows.conf --m ...
- Nuxt.js使用lazyload
Vue的使用方式: 1. 安装插件: npm install vue-lazyload --save-dev 2. main.js引入插件: import VueLazyLoad from 'vue- ...
- Linux C函数库大全
(1)字符测试函数 isalnum(测试字符是否为英文字母或数字) isalpha(测试字符是否为英文字母) isascii(测试字符是否为ASCII码字符) isblank(测试字符是否为空格字符) ...
- 机房收费系统(VB.NET)个人版总结
重构版个人机房收费系统大概从暑假开学開始进行.花了不到一个半月的时间才完毕.以下对我在重构过程中的一写理解. 1.系统设计一个非常重要的目的就是重用.而要做到重用,低耦合是最有效的手段回想一下我们C/ ...
- 一句话说清楚啥是delegate
所谓托付就是类对象调用.托付对象代表随意实现该托付的类的对象.