iOS-响应链(Responder Chain)
2017.05.08 20:40* 字数 1306 阅读 740评论 6喜欢 9
工作接近一年,很久没有更新博客。工作中学到很多知识点后面将花时间整理,作为对一年知识学习的总结:
下面是本篇博客的写作思路:
iOS-Responder Chain.png
人与计算机交互
目前计算机在我们生活中扮演很重要的角色,我们与计算机之间的交互也很普遍。多数情况使用最多的是 PC 和 移动端,而两种交互方式有很大的不同
- PC 与人的交互
- 移动端与人的交互
a) 在 PC 端我们通过键盘、鼠标等来对界面的内容进行操作和完成相关的任务
b)在移动端我们可以通过手指对界面内容进行点击控件和实现相应的操作
这里提出一个疑问,PC 端我们通过点击可以实现控件的相关操作。但是移动端我们手指点击控件是怎么被检测点击的位置、传递信息和做出相应的操作呢?
这里引入一个概念:Responder Chain (响应者链)
Responder Chain (响应者链)
- NSResponder.h 头文件的源码
- UIKit 控件之间的继承关系
NSResponder.h 头文件的源码
在我们点击手机屏幕的一个控件时,与 Responder Chain (响应者链)之间联系紧密。下面是小编在 UIKit 框架中找到相应相关的代码,如下:
@interface UIResponder : NSObject <UIResponderStandardEditActions>
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
- (nullable UIResponder*)nextResponder;
@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO
- (BOOL)canBecomeFirstResponder; // default is NO
- (BOOL)becomeFirstResponder;
@property(nonatomic, readonly) BOOL canResignFirstResponder;
- (BOOL)canResignFirstResponder; // default is YES
- (BOOL)resignFirstResponder;
@property(nonatomic, readonly) BOOL isFirstResponder;
- (BOOL)isFirstResponder;
- (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);
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
在上面 <UIKit/UIResponder.h> 的文件中我们可以看出 UIResponder 的类可以实现 Touch 和 Press 相关操作的做出监听
UIKit 控件之间的继承关系
下面是小编对 UIKit 框架中点击相应控件有关 Class (类)继承关系整理,如下图:
iOS-Responder Chain-UIKit.png
由上面我们可以看出,在界面上我们平时使用控件时通过继承 UIResponder 来实现对界面 Touch 和 Press 相关操作的做出监听
响应链的创建
- 控制器中的 View 树状结构
- 点击相应的测试&打印
控制器中的View树状结构
在手机的 GUI 中我们多数情况下使用 UIViewController 和 UINavigationViewController 控制器来实现界面控件的管理,其中以控制器的 View 为界面显示创建和添加控件,最后形成 View 的控件树状结构
GUI 手机界面中,我们知道 UIView 中用 property (属性) superView 可以用来添加新的 UIView 到当前的 View 中,添加的子类 View 中 property (属性) nextResponder 可以指向父类 View 的 property (属性) superView
每一个 ViewController 中有 property(属性) view -> 既self.view 来进行添加子类 View,同样:view 的 property (属性) 通过 property (属性) nextResponder 来指向 ViewController
下图是 View 初始化布局:
iOS-Responder Chain-View-Front.png
iOS-Responder Chain-View-Hierarchy.png
点击相应的测试&打印
在定义的 ViewA1 中重写touch方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UIResponder *next = [self nextResponder];
NSMutableString *prefix = @"".mutableCopy;
while (nil != next) {
NSLog(@"%@%@", prefix, [next class]);
[prefix appendString:@"--"];
next = [next nextResponder];
}
}
点击 ViewA1 得到的打印结果:
2017-05-08 23:55:12.278 DesignPattern-Create[19441:727956] ViewA0
2017-05-08 23:55:12.278 DesignPattern-Create[19441:727956] --ViewA
2017-05-08 23:55:12.278 DesignPattern-Create[19441:727956] ----UIView
2017-05-08 23:55:12.279 DesignPattern-Create[19441:727956] ------ViewController
2017-05-08 23:55:12.279 DesignPattern-Create[19441:727956] --------UIWindow
2017-05-08 23:55:12.279 DesignPattern-Create[19441:727956] ----------UIApplication
2017-05-08 23:55:12.280 DesignPattern-Create[19441:727956] ------------AppDelegate
基于上面:小编从上面代码打印的结果可以验证,子 View 的 property(属性) nextResponder 指向父 View,父类的 View 的property(属性) nextResponder 最后指向 ViewController
如何找打第一响应者
- 探测器 Hit - Test
- 寻找第一相应的探测猜想
探测器Hit - Test
在响应链查找过程中,有两个函数起到很重要的作用
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
在响应链创建的情况下,我们在把寻找响应者的过程称为:Hit - Testing View 在寻找相应 View 的过程称为:Hit - Test
我们把 Hit - Test 可以比作一个探测器,通过这个探测器来检测手指点击在哪个 UIView 上面
通过上面的继承关系和点击后打印出的结果可以看到,当我们点击界面 UIApplication 就会调用 hitTest: withEvent 查看点击是否在 UIWindow 中如果不在就返回 nil , 如果在 UIWindow 也会调用 hitTest: withEvent 对UIWindow 中 subViews 进行探测
hitTest: withEvent 在探测过程中采用我们比较常见 递归的方式来查找点击 UIView, 通过我们在初始化过程中最后被初始化的放在最上面
在点击 ViewA1 来进行验证下一个 nextResponder 过程中在每一个 View重写 hitTest 如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.hidden || self.alpha <= 0.01) {
return nil;
}
NSLog(@"ResponderChain : %@", [self class]);
if ([self pointInside:point withEvent:event]) {
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
CGPoint converPoin = [subView convertPoint:point fromView:self];
UIView *hitTestView = [subView hitTest:converPoin withEvent:event];
if (hitTestView) {
NSLog(@"ViewA : hitTestView -> %@", [hitTestView class]);
return hitTestView;
}
}
return self;
}
return nil;
}
点击 ViewA1 得到打印的结果如下:
2017-05-09 07:42:05.968 DesignPattern-Create[1197:22554] ResponderChain : ViewB
2017-05-09 07:42:05.969 DesignPattern-Create[1197:22554] ResponderChain : ViewA
2017-05-09 07:42:05.969 DesignPattern-Create[1197:22554] ResponderChain : ViewA0
2017-05-09 07:42:05.969 DesignPattern-Create[1197:22554] ResponderChain : ViewA1
寻找第一相应的探测猜想
根据打印的结果,我们可以验证点击具体查找的流程如下图所示:(实例是点击 viewA1)
UIApplication —> UIWindow —> viewB —> viewA —> viewA0 —> viewA1
iOS-Responder Chain-Hit-Testing-View.png
利用响应者在应用中的体现
- 改变控件的相应范围
改变控件的相应范围
我们可以利用 hitTest 根据 point 方式来重新设置 ponit 的点击相应的范围大小
在 ViewB1 中实现点击相应范围在布局显示基础上下左右拓展 10 px,实现代码如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
NSLog(@"ResponderChain : %@", [self class]);
CGRect touchRect = CGRectInset(self.bounds, -10, -10);
if (CGRectContainsPoint(touchRect, point)) {
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
CGPoint converPoint = [subView convertPoint:point fromView:self];
UIView *hitTestView = [subView hitTest:converPoint withEvent:event];
if (hitTestView) {
NSLog(@"ViewB1 : hitTestView -> %@", [hitTestView class]);
return hitTestView;
}
}
return self;
}
return nil;
}
ViewB1 界面中实际范围,和点击相应的 viewB1 外的虚框的范围也会做出相应的相应。如下图:
iOS-Responder Chain-Apply.png
打印的结果:
2017-05-09 10:34:00.437 DesignPattern-Create[1197:22554] ResponderChain : ViewB
2017-05-09 10:34:00.437 DesignPattern-Create[1197:22554] ResponderChain : ViewB1
2017-05-09 10:34:00.437 DesignPattern-Create[1197:22554] ViewB : hitTestView -> ViewB1
Dome下载地址