你的位置:首页 > 操作系统

[操作系统]你真的了解UIResponder吗?


1:首先查看一下关于UIResponder的定义

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject//响应链中负责传递事件的方法- (nullable UIResponder*)nextResponder;//一个响应对象成为第一响应者的一个前提是它可以成为第一响应者,可以用这个进行判断,默认值为NO- (BOOL)canBecomeFirstResponder;  //如果我们希望将一个响应对象作为第一响应者,则可以使用以下方法,如果对象成为第一响应者,则返回YES;否则返回NO- (BOOL)becomeFirstResponder;//是否可以辞去第一响应者,默认值为YES- (BOOL)canResignFirstResponder; //辞去第一响应者- (BOOL)resignFirstResponder;//判定一个响应对象是否是第一响应者- (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:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//响应移动事件//移动事件开始- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);//移动事件结束- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);//移动事件取消- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);//响应远程控制事件- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);//通过这个方法告诉UIMenuController它内部应该显示什么内容,”复制”、”粘贴”等- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(3_0);//默认的实现是调用canPerformAction:withSender:方法来确定对象是否可以调用action操作。如果我们想要重写目标的选择方式,则应该重写这个方法。- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(7_0);//UIResponder提供了一个只读方法来获取响应链中共享的undo管理器,公共的事件撤销管理者@property(nullable, nonatomic,readonly) NSUndoManager *undoManager NS_AVAILABLE_IOS(3_0);@end

UIResponder提供了几个方法来管理响应链,包括让响应对象成为第一响应者、放弃第一响应者、检测是否是第一响应者以及传递事件到下一响应者的方法;在UIKit中,UIApplication、UIView、UIViewController这几个类都是直接继承自UIResponder类。另外SpriteKit中的SKNode也是继承自UIResponder类。因此UIKit中的视图、控件、视图控制器,以及我们自定义的视图及视图控制器都有响应事件的能力。

 

知识点1:事件分发制机hit-Testing;每当我们点击了一下iOS设备的屏幕,UIKit就会生成一个事件对象UIEvent,然后会把这个Event分发给当前active的app;

告知当前活动的app有事件之后,UIApplication 单例就会从事件队列中去取最新的事件,然后分发给能够处理该事件的对象。这些事件按照先进先出的顺序来处理。当处理事件时,程序的UIApplication对象会从队列头部取出一个事件对象,将其分发出去。UIApplication 获取到Event之后,Application就纠结于到底要把这个事件传递给谁,这时候就要依靠HitTest来决定了。

iOS中,hit-Testing的作用就是找出这个触摸点下面的View是什么,HitTest会检测这个点击的点是不是发生在这个View上,如果是的话,就会去遍历这个View的subviews,直到找到最小的能够处理事件的view,如果整了一圈没找到能够处理的view,则返回自身。

假设我们现在点击到了图中的E,hit-testing将进行如下步骤的检测(不包含重写hit-test并且返回非默认View的情况)

1、触摸点在ViewA内,所以检查ViewA的Subview B、C

2、触摸点不在ViewB内,触摸点在ViewC内部,所以检查ViewC的Subview D、E

3、触摸点不在ViewD内,触摸点发生在ViewE内部,并且ViewE没有subview,所以ViewE属于ViewA中包含这个点的最小单位,所以ViewE变成了该次触摸事件的hit-TestView

PS.

1、默认的hit-testing顺序是按照UIView中Subviews的逆顺序

2、如果View的同级别Subview中有重叠的部分,则优先检查顶部的Subview,如果顶部的Subview返回nil, 再检查底部的Subview

3、Hit-Test也是比较聪明的,检测过程中有这么一点,就是说如果点击没有发生在某View中,那么该事件就不可能发生在View的Subview中,所以检测过程中发现该事件不在ViewB内,也直接就不会检测在不在ViewF内。也就是说,如果你的Subview设置了clipsToBounds=NO,实际显示区域可能超出了superView的frame,你点击超出的部分,是不会处理你的事件的,就是这么任性!

Hit-Test的检查机制如上所示,当确定了Hit-TestView时,如果当前的application没有忽略触摸事件 (UIApplication:isIgnoringInteractionEvents),则application就会去分发事件(sendEvent:->keywindow:sendEvent:)

UIView中提供两个方法用来确定hit-testing View,如下所示

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

当一个View收到hitTest消息时,会调用自己的pointInside:withEvent:方法,如果pointInside返回YES,则表明触摸事件发生在我自己内部,则会遍历自己的所有Subview去寻找最小单位(没有任何子view)的UIView,如果当前View.userInteractionEnabled = NO,enabled=NO(UIControl),或者alpha<=0.01, hidden等情况的时候,hitTest就不会调用自己的pointInside了,直接返回nil,然后系统就回去遍历兄弟节点;

知识点2:事件传递;从上面我们可以知道关于事件的分发制机,并确定好处理事件的hit_text,接着就是处理事件的顺序,正好跟事件分发制机相反,最有机会处理事件的对象是hit-test视图或第一响应者。如果这两者都不能处理事件,UIKit就会将事件传递到响应链中的下一个响应者。每一个响应者确定其是否要处理事件或者是通过nextResponder方法将其传递给下一个响应者。这一过程一直持续到找到能处理事件的响应者对象或者最终没有找到响应者。

 

当系统检测到一个事件时,将其传递给初始对象,这个对象通常是一个视图。然后,会按以下路径来处理事件(我们以上图为例):

1.初始视图(initial view)尝试处理事件。如果它不能处理事件,则将事件传递给其父视图。
2.初始视图的父视图(superview)尝试处理事件。如果这个父视图还不能处理事件,则继续将视图传递给上层视图。
3.上层视图(topmost view)会尝试处理事件。如果这个上层视图还是不能处理事件,则将事件传递给视图所在的视图控制器。
4.视图控制器会尝试处理事件。如果这个视图控制器不能处理事件,则将事件传递给窗口(window)对象。
5.窗口(window)对象尝试处理事件。如果不能处理,则将事件传递给单例app对象。
6.如果app对象不能处理事件,则丢弃这个事件。
从上面可以看到,视图、视图控制器、窗口对象和app对象都能处理事件。另外需要注意的是,手势也会影响到事件的传递。

知识点3:响应链中负责传递事件的方法nextResponder;UIResponder类并不自动保存或设置下一个响应者,该方法的默认实现是返回nil。子类的实现必须重写这个方法来设置下一响应者(如果要实现一个继承于UIResponder,就要重写给它设置一个下一个响应者是谁)。UIView的实现是返回管理它的UIViewController对象(如果它有)或者其父视图。而UIViewController的实现是返回它的视图的父视图;UIWindow的实现是返回app对象;而UIApplication的实现是返回nil。所以,响应链是在构建视图层次结构时生成的。

知识点4:辞去第一响应者resignFirstResponder,resignFirstResponder默认也是返回YES。需要注意的是,如果子类要重写这个方法,则在我们的代码中必须调用super的实现。 

知识点5:默认情况下,多点触摸是被禁用的。为了接受多点触摸事件,我们需要设置响应视图的multipleTouchEnabled属性为YES。

知识点6:默认情况下,程序的每一个window都有一个undo管理器,它是一个用于管理undo和redo操作的共享对象。然而,响应链上的任何对象的类都可以有自定义undo管理器。例如,UITextField的实例的自定义管理器在文件输入框放弃第一响应者状态时会被清理掉。当需要一个undo管理器时,请求会沿着响应链传递,然后UIWindow对象会返回一个可用的实例。

知识点7:canPerformAction结合UIMenuController的运用

UIMenuController是UIKit里面的控件,UIMenuController的作用在开发中弹出的菜单栏,包含那些复制、粘贴等,也可以自定义要弹出的选择及操作;UITextField、UIWebView、UITextView自带有这种UIMenuController效果;

实例1:在Label中的运用,并且修改的UIMenuController选择内容#import "JHLabel.h"@implementation JHLabel/** 不管控件是通过xib stroyboard 还是纯代码 提供两种初始化的操作都调用同一个方法 */- (instancetype)initWithFrame:(CGRect)frame{  if (self = [super initWithFrame:frame]) {    [self setupTap];  }  return self;}/** 不管控件是通过xib stroyboard 还是纯代码 提供两种初始化的操作都调用同一个方法 */- (void)awakeFromNib{  [self setupTap];}/** 设置敲击手势 */- (void)setupTap{  self.text = @"author:会跳舞的狮子";  //已经在stroyboard设置了与用户交互,也可以用纯代码设置//  self.userInteractionEnabled = YES;  //当前控件是label 所以是给label添加敲击手势  [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(labelClick)]];}/** 点击label触发的方法 */- (void)labelClick{  // 让label成为第一响应者 \  一定要写这句话 因为这句话才是主动让label成为第一响应者  [self becomeFirstResponder];  // 获得菜单  UIMenuController *menu = [UIMenuController sharedMenuController];  // 设置菜单内容 \  因为menuItems是数组 官方没有给出需要传入什么对象,但是以经验可以判断出需要传入的是UIMenuItem对象 \  而且显示是按顺序的  menu.menuItems = @[            [[UIMenuItem alloc] initWithTitle:@"顶" action:@selector(ding:)],            [[UIMenuItem alloc] initWithTitle:@"回复" action:@selector(reply:)],            [[UIMenuItem alloc] initWithTitle:@"举报" action:@selector(warn:)]            ];  // 菜单最终显示的位置 \  有两种方式: 一种是以自身的bounds 还有一种是以父控件的frame   [menu setTargetRect:self.bounds inView:self];//  [menu setTargetRect:self.frame inView:self.superview];  // 显示菜单  [menu setMenuVisible:YES animated:YES];}#pragma mark - UIMenuController相关/** * 让Label具备成为第一响应者的资格 */- (BOOL)canBecomeFirstResponder{  return YES;}/** * 通过第一响应者的这个方法告诉UIMenuController可以显示什么内容 */- (BOOL)canPerformAction:(SEL)action withSender:(id)sender{  if ( (action == @selector(copy:) && self.text) // 需要有文字才能支持复制    || (action == @selector(cut:) && self.text) // 需要有文字才能支持剪切    || action == @selector(paste:)    || action == @selector(ding:)    || action == @selector(reply:)    || action == @selector(warn:)) return YES;  return NO;}#pragma mark - 监听MenuItem的点击事件/** 剪切 */- (void)cut:(UIMenuController *)menu{  //UIPasteboard 是可以在应用程序与应用程序之间共享的 \  (应用程序:你的app就是一个应用程序 比如你的QQ消息可以剪切到百度查找一样)  // 将label的文字存储到粘贴板  [UIPasteboard generalPasteboard].string = self.text;  // 清空文字  self.text = nil;}/** 赋值 */- (void)copy:(UIMenuController *)menu{  // 将label的文字存储到粘贴板  [UIPasteboard generalPasteboard].string = self.text;}/** 粘贴 */- (void)paste:(UIMenuController *)menu{  // 将粘贴板的文字赋值给label  self.text = [UIPasteboard generalPasteboard].string;}//如果方法不实现,是不会显示出来的- (void)ding:(UIMenuController *)menu{  NSLog(@"%s %@", __func__, menu);}- (void)reply:(UIMenuController *)menu{  NSLog(@"%s %@", __func__, menu);}- (void)warn:(UIMenuController *)menu{  NSLog(@"%s %@", __func__, menu);}@end

知识点8:禁用一些长按的操作设置

代码1:禁用所有长按文本框操作#pragma mark - 禁用所有长按文本框操作- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {  if ([UIMenuController sharedMenuController]) {    [UIMenuController sharedMenuController].menuVisible = NO;  }  return NO;}

代码2:禁用部分长按文本框操作#pragma mark - 禁用部分长按文本框操作- (BOOL)canPerformAction:(SEL)action withSender:(id)sender{  //禁用选择、全选、粘贴功能  //...  if (action == @selector(paste:))    return NO;  if (action == @selector(select:))    return NO;  if (action == @selector(selectAll:))    return NO;  //...  return [super canPerformAction:action withSender:sender];}

实例:#import <UIKit/UIKit.h>@interface DDTextField : UITextField@end#import "DDTextField.h"@implementation DDTextField#pragma mark -- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {  if (action == @selector(paste:))    return YES;  if (action == @selector(cut:))    return YES;  if (action == @selector(copy:))    return YES;  if (action == @selector(select:))    return YES;  if (action == @selector(selectAll:))    return YES;  return NO;}@end

2:访问快捷键命令

我们的应用可以支持外部设备,包括外部键盘。在使用外部键盘时,使用快捷键可以大大提高我们的输入效率。因此从iOS7后,UIResponder类新增了一个只读属性keyCommands,来定义一个响应者支持的快捷键;

typedef NS_OPTIONS(NSInteger, UIKeyModifierFlags) {  UIKeyModifierAlphaShift   = 1 << 16, // Alppha+Shift键  UIKeyModifierShift     = 1 << 17, //Shift键  UIKeyModifierControl    = 1 << 18, //Control键  UIKeyModifierAlternate   = 1 << 19, //Alt键  UIKeyModifierCommand    = 1 << 20, //Command键  UIKeyModifierNumericPad   = 1 << 21,  //Num键} NS_ENUM_AVAILABLE_IOS(7_0);//按键命令类:NS_CLASS_AVAILABLE_IOS(7_0) @interface UIKeyCommand : NSObject <NSCopying, NSSecureCoding>- (instancetype)init NS_DESIGNATED_INITIALIZER;- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;//输入字符串@property (nonatomic,readonly) NSString *input;//按键调节器@property (nonatomic,readonly) UIKeyModifierFlags modifierFlags;//按指定调节器键输入字符串并设置事件@property (nullable,nonatomic,copy) NSString *discoverabilityTitle NS_AVAILABLE_IOS(9_0);+ (UIKeyCommand *)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action;// Key Commands with a discoverabilityTitle _will_ be discoverable in the UI.+ (UIKeyCommand *)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action discoverabilityTitle:(NSString *)discoverabilityTitle NS_AVAILABLE_IOS(9_0);@end

知识点1:一个支持硬件键盘命令的响应者对象可以重新定义这个方法并使用它来返回一个其所支持快捷键对象(UIKeyCommand)的数组(在UIResponder (UIResponderKeyCommands)分类中)。每一个快捷键命令表示识别的键盘序列及响应者的操作方法。我们用这个方法返回的快捷键命令数组被用于整个响应链。当与快捷键命令对象匹配的快捷键被按下时,UIKit会沿着响应链查找实现了响应行为方法的对象。它调用找到的第一个对象的方法并停止事件的处理。

3:UIResponder (UIResponderKeyCommands)分类

//响应者类的按键命令类类目@interface UIResponder (UIResponderKeyCommands)//组合快捷键命令(装有多个按键的数组)@property (nullable,nonatomic,readonly) NSArray<UIKeyCommand *> *keyCommands NS_AVAILABLE_IOS(7_0); // returns an array of UIKeyCommand objects<@end@interface NSObject(UIResponderStandardEditActions)  // these methods are not implemented in NSObject//剪贴- (void)cut:(nullable id)sender NS_AVAILABLE_IOS(3_0);//复制- (void)copy:(nullable id)sender NS_AVAILABLE_IOS(3_0);//粘贴- (void)paste:(nullable id)sender NS_AVAILABLE_IOS(3_0);//选择- (void)select:(nullable id)sender NS_AVAILABLE_IOS(3_0);//选择全部- (void)selectAll:(nullable id)sender NS_AVAILABLE_IOS(3_0);//删除- (void)delete:(nullable id)sender NS_AVAILABLE_IOS(3_2);//从左到右写入字符串(居左)- (void)makeTextWritingDirectionLeftToRight:(nullable id)sender NS_AVAILABLE_IOS(5_0);//从右到左写入字符串(居右)- (void)makeTextWritingDirectionRightToLeft:(nullable id)sender NS_AVAILABLE_IOS(5_0);//切换字体为黑体(粗体)- (void)toggleBoldface:(nullable id)sender NS_AVAILABLE_IOS(6_0);//切换字体为斜体- (void)toggleItalics:(nullable id)sender NS_AVAILABLE_IOS(6_0);//给文字添加下划线- (void)toggleUnderline:(nullable id)sender NS_AVAILABLE_IOS(6_0);//增加字体大小- (void)increaseSize:(nullable id)sender NS_AVAILABLE_IOS(7_0);//减小字体大小- (void)decreaseSize:(nullable id)sender NS_AVAILABLE_IOS(7_0);@end

4:UIResponder (UIResponderInputViewAdditions)分类

@class UIInputViewController;@class UITextInputMode;@class UITextInputAssistantItem;@interface UIResponder (UIResponderInputViewAdditions)//键盘输入视图(系统默认的,可以自定义)@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputView NS_AVAILABLE_IOS(3_2);//弹出键盘时附带的视图@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputAccessoryView NS_AVAILABLE_IOS(3_2);@property (nonnull, nonatomic, readonly, strong) UITextInputAssistantItem *inputAssistantItem NS_AVAILABLE_IOS(9_0) __WATCHOS_PROHIBITED;//键盘输入视图控制器 IOS8以后@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputViewController NS_AVAILABLE_IOS(8_0);//弹出键盘时附带的视图的视图控制器 IOS8以后@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputAccessoryViewController NS_AVAILABLE_IOS(8_0);//文本输入模式@property (nullable, nonatomic, readonly, strong) UITextInputMode *textInputMode NS_AVAILABLE_IOS(7_0);//文本输入模式标识@property (nullable, nonatomic, readonly, strong) NSString *textInputContextIdentifier NS_AVAILABLE_IOS(7_0);//根据设置的标识清除指定的文本输入模式+ (void)clearTextInputContextIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(7_0);//重新刷新键盘输入视图- (void)reloadInputViews NS_AVAILABLE_IOS(3_2);@end

知识点1:UITextFields和UITextView有一个inputAccessoryView的属性,当你想在键盘上展示一个自定义的view时,你就可以设置该属性。你设置的view就会自动和键盘keyboard一起显示了。

需要注意的是,你所自定义的view既不应该处在其他的视图层里,也不应该成为其他视图的子视图。其实也就是说,你所自定义的view只需要赋给属性inputAccessoryView就可以了,不要再做其他多余的操作。

我们在使用UITextView和UITextField的时候,可以通过它们的inputAccessoryView属性给输入时呼出的键盘加一个附属视图,通常是UIToolBar,用于回收键盘。

inputView就是显示键盘的view,如果重写这个view则不再弹出键盘,而是弹出自己的view.如果想实现当某一控件变为第一响应者时不弹出键盘而是弹出我们自定义的界面,那么我们就可以通过修改这个inputView来实现,比如弹出一个日期拾取器。

inputView不会随着键盘出现而出现,设置了InputView只会当UITextField或者UITextView变为第一相应者时显示出来,不会显示键盘了。设置了InputAccessoryView,它会随着键盘一起出现并且会显示在键盘的顶端。InutAccessoryView默认为nil.

5:UIResponder (ActivityContinuation)分类

// 按键输入箭头指向UIKIT_EXTERN NSString *const UIKeyInputUpArrow     NS_AVAILABLE_IOS(7_0);UIKIT_EXTERN NSString *const UIKeyInputDownArrow    NS_AVAILABLE_IOS(7_0);UIKIT_EXTERN NSString *const UIKeyInputLeftArrow    NS_AVAILABLE_IOS(7_0);UIKIT_EXTERN NSString *const UIKeyInputRightArrow   NS_AVAILABLE_IOS(7_0);UIKIT_EXTERN NSString *const UIKeyInputEscape     NS_AVAILABLE_IOS(7_0);//响应者类的类目:@interface UIResponder (ActivityContinuation)//用户活动@property (nullable, nonatomic, strong) NSUserActivity *userActivity NS_AVAILABLE_IOS(8_0);//更新用户活动- (void)updateUserActivityState:(NSUserActivity *)activity NS_AVAILABLE_IOS(8_0);//恢复用户活动- (void)restoreUserActivityState:(NSUserActivity *)activity NS_AVAILABLE_IOS(8_0);@end

知识点1:支持User Activities,从iOS 8起,苹果为我们提供了一个非常棒的功能,即Handoff。使用这一功能,我们可以在一部iOS设备的某个应用上开始做一件事,然后在另一台iOS设备上继续做这件事。Handoff的基本思想是用户在一个应用里所做的任何操作都可以看作是一个Activity,一个Activity可以和一个特定iCloud用户的多台设备关联起来。在编写一个支持Handoff的应用时,会有以下三个交互事件:

  • 为将在另一台设备上继续做的事创建一个新的User Activity;
  • 当需要时,用新的数据更新已有的User Activity;
  • 把一个User Activity传递到另一台设备上。

为了支持这些交互事件,在iOS 8后,UIResponder类新增了几个方法,我们在此不讨论这几个方法的实际使用,想了解更多的话,可以参考iOS 8 Handoff 开发指南。我们在此只是简单描述一下这几个方法。

在UIResponder中,已经为我们提供了一个userActivity属性,它是一个NSUserActivity对象。因此我们在UIResponder的子类中不需要再去声明一个userActivity属性,直接使用它就行。其声明如下:

@property(nonatomic, retain) NSUserActivity *userActivity

由UIKit管理的User Activities会在适当的时间自动保存。一般情况下,我们可以重写UIResponder类的updateUserActivityState:方法来延迟添加表示User Activity的状态数据。当我们不再需要一个User Activity时,我们可以设置userActivity属性为nil。任何由UIKit管理的NSUserActivity对象,如果它没有相关的响应者,则会自动失效。

另外,多个响应者可以共享一个NSUserActivity实例。

上面提到的updateUserActivityState:是用于更新给定的User Activity的状态。其定义如下:

- (void)updateUserActivityState:(NSUserActivity *)activity

子类可以重写这个方法来按照我们的需要更新给定的User Activity。我们需要使用NSUserActivity对象的addUserInfoEntriesFromDictionary:方法来添加表示用户Activity的状态。

在我们修改了User Activity的状态后,如果想将其恢复到某个状态,则可以使用以下方法:

- (void)restoreUserActivityState:(NSUserActivity *)activity

子类可以重写这个方法来使用给定User Activity的恢复响应者的状态。系统会在接收到数据时,将数据传递给application:continueUserActivity:restorationHandler:以做处理。我们重写时应该使用存储在user activity的userInfo字典中的状态数据来恢复对象。当然,我们也可以直接调用这个方法。