iOS文档补完计划--UIResponder

UIResponder主要是负责响应我们屏幕上各种事件、并维护一个响应链的机制。
日常工作中我们主要用到:
响应链及其管理、第一响应者、响应触摸事件、验证命令(Menu菜单)、管理输入视图(自定义键盘)等等。


目录

  • UIResponder
  • 响应链
  • 管理响应者链
    • nextResponder
    • isFirstResponder
    • canBecomeFirstResponder
    • becomeFirstResponder
    • canBecomeFirstResponder
    • resignFirstResponder
  • 响应触摸事件
    • touchesBegan:withEvent:
    • touchesMoved:withEvent:
    • touchesEnded:withEvent:
    • touchesCancelled:withEvent:
    • touchesEstimatedPropertiesUpdated:
    • touches参数
    • UITouch
    • UIEvent
  • 响应动作事件
    • motionBegan:withEvent:
    • motionEnded:withEvent:
    • motionCancelled:withEvent:
  • 响应远程控制事件
    • remoteControlReceivedWithEvent:
  • 验证命令
    • canPerformAction:withSender:
    • targetForAction:withSender:
    • 关于菜单(UIMenuController)
  • 撤消管理器
    • undoManager
  • 访问快捷键命令
    • keyCommands
  • 管理输入视图
    • inputView
    • inputAccessoryView
    • inputViewController
    • inputAccessoryViewController
    • reloadInputViews
  • 管理文本输入模式
    • textInputMode
    • textInputContextIdentifier
    • clearTextInputContextIdentifier
    • inputAssistantItem
  • User Activities
    • userActivity
    • updateUserActivityState:
    • restoreUserActivityState:
  • UIEventSubtype
  • UITouch
  • UIEvent
  • Touch Event、UIControl、UIGestureRecognizer三兄弟的恩怨情仇

UIResponder

在UIKit中,UIApplication、UIView、UIViewController这几个类都是直接继承自UIResponder类。另外SpriteKit中的SKNode也是继承自UIResponder类。因此UIKit中的视图、控件、视图控制器,以及我们自定义的视图及视图控制器都有响应事件的能力。这些对象通常被称为响应对象,或者是响应者(以下我们统一使用响应者)。


响应链

这个响应链指的的是处理点击事件的链条(在此之前还要经历由主窗口向上遍历找到触摸点最终控件的过程)。通常第一个响应者位于层级最上方、然后是其俯视图以此类推、链条末端为UIApplication对象。

你可以参考这篇文章《史上最详细的iOS之事件的传递和响应机制-原理篇》《iOS触摸事件全家桶》

文章很杂、所以简单总结一下:
对于事件(注意我没说包括手势和UIControl啊这两个我还得研究研究)而言、分为两个阶段:

  1. 事件产生、自下而上寻找最合适的View
    UIApplication -> UIWindow -> 父View -> 子view
  2. 找到最合适的View、进入响应链进行自上而下处理
    可以参照下面对于nextResponder的解释。如果响应者链上有一个响应者可以处理该事件(实现了touch方法)。则交由他处理、否则最后将被UIApplication丢弃。

此外、如果控件的userInteractionEnabledNO、是不会进入响应链中的(但是hidden和alpha不影响、即使看不见控件也可以使其成为第一响应者)。


管理响应者链

我们可以让一个UIResponder对象参与到响应链中、以便让他响应一些事件(比如手机摇动、弹起键盘)等等。

  • - nextResponder
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;

返回响应者链中的下一个对象、如果没有则返回nil。

UIResponder本身并不知道谁才是下一个响应者、所以如果想使用这个方法我们必须重载其子类的get方法

对于UIView及其子类、默认的实现是这样:

  1. 如果是控制器的View(也就是通常的self.view)、返回控制器。
  2. 反之、返回其父视图(superview)。
  3. 如果都不满足、返回nil。

利用这个特性、我们可以查找UIView的根控制器

UIView * view  = [UIView new];
[self.view addSubview:view];

UIViewController * controller;
id next = [view nextResponder];
while(![next isKindOfClass:[ViewController class]] && next)
{
    NSLog(@"%@",next);
    next = [next nextResponder];
}
if ([next isKindOfClass:[ViewController class]])
{
    controller = (ViewController *)next;
}

还可以将事件一级一级往下传

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 这里可以做子view自己想做的事,做完后,事件继续上传,就可以让其父类,甚至父viewcontroller获取到这个事件了
    [[self nextResponder] touchesBegan:touches withEvent:event];
}

对于UIViewController:
会返回vc.view.superview、如果没有则为nil。

UIViewController * vc = [UIViewController new];
[self.view addSubview:vc.view];

NSLog(@"\nself.view--%@\nself.view.superview--%@\n[self nextResponder]--%@\nvc.view.superview--%@\n[vc nextResponder]--%@",self.view,self.view.superview,[self nextResponder],vc.view.superview,[vc nextResponder]);

//打印结果:
self.view--<UIView: 0x7fbe9841b3b0; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x60000002c360>>
self.view.superview--(null)
[self nextResponder]--(null)
vc.view.superview--<UIView: 0x7fbe9841b3b0; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x60000002c360>>
[vc nextResponder]--<UIView: 0x7fbe9841b3b0; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x60000002c360>>

对于UIWindow:
返回UIApplication对象

对于UIApplication:
返回nil。也就是丢弃该事件的响应机会。

  • - isFirstResponder
@property(nonatomic, readonly) BOOL isFirstResponder;

判定是否为第一响应者

我个人只知道对UITextField有用。其他的还望补充。

  • - canBecomeFirstResponder
@property(nonatomic, readonly) BOOL canBecomeFirstResponder;

返回视图能否被作为第一响应者

默认(以及UIView/UIButton等)为NO、有键盘的控件(不排除还有其他的)为YES。

canBecomeFirstResponder返回YES是让对象能够调用becomeFirstResponder的先决条件。毕竟很多控件只希望处理与自己有关的事件而生(比如UIButton)。

  • - becomeFirstResponder
- (BOOL)becomeFirstResponder;

尝试让对象称为第一响应者。如果他当前已经是则返回YES、反之为NO。

就想之前所说、称为第一响应者并不代表能够拦截上层的点击事件。只是对于一些特殊事件(键盘、摇动等等)具有优先响应权。

  • - canBecomeFirstResponder
@property(nonatomic, readonly) BOOL canResignFirstResponder;  

返回视图能否放弃第一响应者

官方文档来看~默认都是YES。

你可以通过重载这个方法来让键盘无法回收、知道用户输入了你合适的内容(叫爸爸??)。

  • - resignFirstResponder
- (BOOL)resignFirstResponder;

如果已经是第一响应者、调用会返回YES并且取消其第一响应者的位置。否则NO。

重载的效果同上、不允许其放弃响应权限。

第一响应对象和其他响应对象之间有什么区别?对于普通的触摸事件没什么区别。就算我把一个按钮设置成第一响应对象,当我点击其他按钮时,还是会响应其他按钮,而不会优先响应第一响应对象。
第一响应对象的区别在于负责处理那些和屏幕位置无关的事件,例如摇动、键盘输入、控制面板(就是下滑出来的那个?)。
苹果官方文档的说法是:第一响应对象是窗口中,应用程序认为最适合处理事件的对象

1. 另外需要注意的是只有当视图是视图层次结构的一部分时才调用(所有和FirstResponder有关的)方法、否则很有可能返回一个未知结果。
如果视图的window属性(这个我回头要去看看文档)不为空时、视图才在一个视图层次结构中;如果该属性为nil、则视图不在任何层次结构中。

2. 隐藏控件并不能取消其第一响应者的权限
隐藏UITextField、键盘不会自动落下。你需要手动resignFirstResponder才行。


响应触摸事件

负责处理屏幕的触摸事件

这四个方法默认都是什么都不做。
不过、UIKit中UIResponder的子类、比如UIView、对于这几个方法的实现都会把消息传递到响应链上。
而对于UIControl、内部处理会直接影响到TouchDown以及TouchUpInside等状态以及相关方法的触发、并且不再向下传递。所以一定要调用super、以保证Target-Action的正确触发

一旦响应链中有人重载了touch方法、则由其处理、不再传递。
因此、为了不阻断响应链、我们的子类在重写时需要调用父类的相应方法。而不要将消息直接发送给下一响应者(除非你能够确定该类只做了这个操作)。

  • - touchesBegan:withEvent:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

一个或多个手指在视图或窗口上触摸

  • - touchesMoved:withEvent:
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

一个或多个手指在视图或窗口上移动

  • - touchesEnded:withEvent:
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

一个或多个手指从视图或窗口上抬起

  • - touchesCancelled:withEvent:
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

当触摸事件被系统事件取消(打断)

需要注意、这里不只是打电话等等。手势事件也可能会打断响应链的传递。因为手势比响应链拥有更高的优先级、这也是为什么添加了手势的View会阻止子View响应链的原因。《iOS触摸事件那点儿事》/《iOS点击事件和手势冲突》

  • - touchesEstimatedPropertiesUpdated:
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

3D Touch相关方法。包括有一些触摸属性UITouchPropertyie、比如压力大小等等。

  • touches参数
  1. 当用一根手指触摸屏幕时
    会创建一个与手指相关联的UITouch对象。
  2. 当用两根手指同时触摸屏幕
    则会调用一次touchesBegan方法、创建两个UITouch对象。
  3. 当不是同时触摸
    调用两次方法,每次的touches参数都只有一个UITouch对象。

那么、如何判断双指触控?

  1. 同时触摸:
    NSSet有多少个UITouch对象元素
  2. 先后触摸:
    可以参考一下《iOS 触摸事件之双指先后触摸问题的解决》。其一是对touchesBegan方法进行计数、其二是直接注册UIPanGestureRecognizer的两指事件。
  • UITouch

UITouch保存着跟本次手指触摸相关的信息

  1. 一个手指第一次点击屏、会形成一个UITouch对象、直到离开销毁
    当手指移动时、系统会更新同一个UITouch对象、使之能够一直保存该手指的触摸位置。(也就是说上面四个方法、很大概率还是同一个UITouch)
  2. 当前手指触碰的屏幕位置等信息
    触摸的位置、时间、次数等。
  3. UITouch对象的TouchPhase保存当前状态
    包括开始触碰、移动、保持、离开、被取消。
  • UIEvent

第一个手指开始触摸屏幕到最后一个手指离开屏幕定义为一个触摸事件

  1. UIEvent实际可以包括多个UITouch对象
    有几个手指触碰,就会有几个UITouch对象。
  2. UITouch对象包括当前手指触碰的屏幕位置等信息
  3. 一次完整的触摸过程中、只会产生一个事件对象
    4个触摸方法都是同一个event参数。

响应动作事件

  1. 负责处理设备的动作事件、比如摇一摇。
    参数motion是一个UIEventSubtype类型结构体。
    就我目前的理解、只有UIEventSubtypeMotionShake也就是摇晃事件会走到几个方法里。
  2. 和触碰事件一样、这几个方法的默认操作也是什么都不做。不过,UIKit中UIResponder的子类,尤其是UIView,这几个方法的实现都会把消息传递到响应链上。
  3. 想要响应设备的动作事件、该对象必须为第一响应者
  • - motionBegan:withEvent:
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);

动作事件开始

  • - motionEnded:withEvent:
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);

动作事件结束

  • - motionCancelled:withEvent:
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);

动作事件被系统事件打断(参考touch事件)


响应远程控制事件

  • - remoteControlReceivedWithEvent:
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);

远程控制事件来源于一些外部的配件,如耳机等。用户可以通过耳机来控制视频或音频的播放。

参数event.subtype是一个UIEventSubtype类型的枚举。我们可以根据该属性来实现具体操作。

需要注意的是

想要响应远程控制、你必须启动远程控制[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
相对的、如果不想响应了也需要关闭[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
此外、有人说如果是UIViewController或者UIApplication、不必成为第一响应者也可以接收远程事件。(这个我需要有空去真机上测一测)


验证命令

在工作中经常要处理需要菜单UIMenuController命令、如复制粘贴等。以下两个方法为此服务。

  • - canPerformAction:withSender:
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(3_0);

返回这个类是否支持某种菜单操作。

需要注意的是这个方法只能决定本对象是否支持、但具体结果会受整条响应链影响。

  • - targetForAction:withSender:
- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(7_0);

返回能够响应action事件的对象

内部的默认实现是调用自身的canPerformAction:withSender方法、如果是YES则返回self。否则在响应链上继续用targetForAction查询。
这个方法中返回的对象、会负责点击按钮的方法实现。

所以、我们如果想从根本上杜绝某个action。应该将第一响应对象的某个action在targetForAction中返回nil。而不是在canPerformAction中返回NO(因为实际结果受到响应链其他独享的影响)。

对于action参数、有两种可能性:

这里第一种是一定会执行的、也就是会把UIResponderStandardEditActions的协议方法都轮询一遍

  1. UIResponderStandardEditActions协议中定义的方法。
    如果返回YES、Menu会自动为我们添加对应的按钮。(当然、具体实现还是需要我们自己写的)。
  2. 自定义aciton
UIMenuItem *copyItem = [[UIMenuItem alloc] initWithTitle:@"copy" action:@selector(copyAciton:)];
[[UIMenuController sharedMenuController] setMenuItems:@[copyItem]];
  • 关于菜单(UIMenuController)
  1. 我们可以在一个长安手势方法中添加如下代码显示菜单
[[UIMenuController sharedMenuController] setTargetRect:self.frame inView:self.superview];
[[UIMenuController sharedMenuController] setMenuVisible:YES animated: YES];
  1. Menu会去调用第一响应者的targetForAction:withSender:方法
    决定需要展示哪些按钮。

  2. 点击按钮之后执行会再次向第一响应者查询targetForAction:withSender:
    决定该由哪个对象进行处理。

  3. 关于这里为什么没说canPerformAction:withSender:以及响应链、请返回去看targetForAction:withSender:的内部实现。


撤消管理器

  • undoManager
@property(nonatomic, readonly) NSUndoManager *undoManager;

被用做撤消和反撤消功能

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


访问快捷键命令

  • keyCommands
@property (nullable,nonatomic,readonly) NSArray<UIKeyCommand *> *keyCommands NS_AVAILABLE_IOS(7_0); // returns an array of UIKeyCommand objects<

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

我们用这个方法返回的快捷键命令数组被用于整个响应链。当与快捷键命令对象匹配的快捷键被按下时,UIKit会沿着响应链查找实现了响应行为方法的对象。它调用找到的第一个对象的方法并停止事件的处理。可以参阅:《你真的了解UIResponder吗?》中相关的部分。


管理输入视图

所谓的输入视图、是指当对象为第一响应者时、显示另外一个视图用来处理当前对象的信息输入、如UITextView和UITextField两个对象。

在其成为第一响应者时、会显示一个系统键盘、用来输入信息。这个系统键盘就是输入视图。输入视图有两种,一个是inputView,另一个是inputAccessoryView。这两者如图所示:


对于UITextField和UITextField以外的控件,inputView和inputAccessoryView是只读的、当然你可以继承某个控件、然后重载这两个属性。

  • inputView
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputView NS_AVAILABLE_IOS(3_2);

当接收者成为第一个响应者时显示的自定义输入视图

  • inputAccessoryView
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputAccessoryView NS_AVAILABLE_IOS(3_2);

当接收器成为第一响应者时显示的自定义输入附件视图

  • inputViewController
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputViewController NS_AVAILABLE_IOS(8_0);

自定义输入视图控制器在接收器成为第一响应者时使用

  • inputAccessoryViewController
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputAccessoryViewController NS_AVAILABLE_IOS(8_0);

自定义输入附件视图控制器,用于在接收器成为第一响应者时显示

  • reloadInputViews
- (void)reloadInputViews NS_AVAILABLE_IOS(3_2);

更新其自定义输入和附件视图(只对第一响应者起作用)

用处的话、我只找到一个切换键盘、欢迎大佬补充。

if (seg.selectedSegmentIndex == 1) {
    self.xfg_keyboard = [[XFG_KeyBoard alloc] initWithNumber:@2];
    self.textField.inputView = self.xfg_keyboard;
    self.xfg_keyboard.delegate = self;
    [self.textField reloadInputViews];
    
  }
    
if (seg.selectedSegmentIndex == 2) {
    self.xfg_keyboard = [[XFG_KeyBoard alloc] initWithNumber:@3];
    self.textField.inputView = self.xfg_keyboard;
    self.xfg_keyboard.delegate = self;
    [self.textField reloadInputViews];
}
- 输入控制器和输入视图的区别

输入控制器包含一个输入视图、并且由很多方法提供键盘支持。相对的、输入视图则只是一个普通的View。可能你需要用代理的方式将点击事件进行传递。《iOS8新特性扩展(Extension)应用之四——自定义键盘控件》

通过以上的几个属性、你可以尝试(我自己没试、因为没这些需求~)
自定义键盘:《深入讲解iOS键盘三:自定义键盘的两种方法》
让任意控件弹起键盘:《iOS开发inputView和inputAccessoryView》
更好的支持展示字符串类型的表情《谈UITextView、UITextField的InPutView和AccessoryInputView的便利》


管理文本输入模式

键盘次序可以看做一个队列,UIKit 有公共的键盘次序。但我们可以针对UIResponder子类进行一些特化

  • textInputMode
@property (nullable, nonatomic, readonly, strong) UITextInputMode *textInputMode NS_AVAILABLE_IOS(7_0);

指定 UIResponder 调起键盘时候显示的键盘类型。忽略公共的键盘次序

注意这里是调起键盘时的样式、而不是永久的样式。

//无视系统顺序、修改调起键盘样式
- (UITextInputMode *)textInputMode {
    static UITextInputMode * emojiMode;
    if (emojiMode) {
        return emojiMode;
    }
    for (UITextInputMode * inputModel in [UITextInputMode activeInputModes]) {
        NSLog(@"%@",inputModel.primaryLanguage);
        if ([inputModel.primaryLanguage isEqualToString:@"emoji"]) {
            emojiMode = inputModel;
        }
        
    }
    return emojiMode;
}

//打印
2018-09-03 10:26:33.727471+0800 NSObject[20135:1592579] zh-Hans
2018-09-03 10:26:33.727626+0800 NSObject[20135:1592579] en-US
2018-09-03 10:26:33.727733+0800 NSObject[20135:1592579] emoji
  • textInputContextIdentifier
@property (nullable, nonatomic, readonly, strong) NSString *textInputContextIdentifier NS_AVAILABLE_IOS(7_0);

这个属性,只要它不为空,那么相当于开启了对本responder的textInputMode的实时保存。每次在该responder变成第一responder的时候,弹出的键盘都会是上一次选择的那种键盘。

存疑、试了试好像不重载也会被记录啊

  • + clearTextInputContextIdentifier:
+ (void)clearTextInputContextIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(7_0);

调用这个方法可以从程序的user default中移除与指定标识相关的所有文本输入模式。移除这些信息会让响应者重新使用默认的文本输入模式。

和上一个Identifier一样没搞懂、期待有大神指正

  • inputAssistantItem
- (void)reloadInputViews NS_AVAILABLE_IOS(3_2);

iPad键盘上方的快捷方式栏。iPhone或iPod Touch上没有快捷方式栏

包含用于管理文本的输入建议和其他控件,例如剪切,复制和粘贴命令


User Activities

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

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

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

  • userActivity
@property (nullable, nonatomic, strong) NSUserActivity *userActivity NS_AVAILABLE_IOS(8_0);

由UIKit管理的User Activities

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

  • - updateUserActivityState:
- (void)updateUserActivityState:(NSUserActivity *)activity NS_AVAILABLE_IOS(8_0);

更新用户的活动。

你也可以理解成发送、这个方法会由系统自动调用。
你可以向这个avtivity添加数据然后调用super进行发送。

  • - restoreUserActivityState:
- (void)restoreUserActivityState:(NSUserActivity *)activity NS_AVAILABLE_IOS(8_0);

恢复用户的操作

你可以在这里获取到userActivity用于恢复用户操作。
这个方法需要被子类重写、并且由AppDelegate调用

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler
{
    UINavigationController *navi = (UINavigationController *)self.window.rootViewController;
    [navi.topViewController restoreUserActivityState:userActivity];
    
    return YES;
}

对于iOS9、NSUserActivity还有另一个用途、Search API。就是类似在主屏幕搜索微博内容的功能。
或许你可以参考一下《iOS Search API - NSUserActivity》


UITouch

//触摸事件在屏幕上有一个周期
typedef NS_ENUM(NSInteger, UITouchPhase) {
    UITouchPhaseBegan,           //开始触摸  
    UITouchPhaseMoved,           //移动    
    UITouchPhaseStationary,      //停留
    UITouchPhaseEnded,            //触摸结束
    UITouchPhaseCancelled,       //触摸中断
};

//检测是否支持3DTouch
typedef NS_ENUM(NSInteger, UIForceTouchCapability) {
    UIForceTouchCapabilityUnknown = 0,  //3D Touch检测失败
    UIForceTouchCapabilityUnavailable = 1,  //3D Touch不可用
    UIForceTouchCapabilityAvailable = 2  //3D Touch可用
};

NS_CLASS_AVAILABLE_IOS(2_0) @interface UITouch : NSObject

//触摸产生或变化的时间戳 只读
@property(nonatomic,readonly) NSTimeInterval      timestamp;
//触摸周期内的各个状态
@property(nonatomic,readonly) UITouchPhase        phase;
//短时间内点击的次数 只读
@property(nonatomic,readonly) NSUInteger          tapCount;   

//获取手指与屏幕的接触半径 IOS8以后可用 只读
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
//获取手指与屏幕的接触半径的误差 IOS8以后可用 只读
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);

//触摸时所在的窗口 只读
@property(nullable,nonatomic,readonly,strong) UIWindow                        *window;
//触摸时所在视图
@property(nullable,nonatomic,readonly,strong) UIView                          *view;
//获取触摸手势
@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);

//取得在指定视图的位置
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0,0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(nullable UIView *)view;
//该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;

//获取触摸压力值,一般的压力感应值为1.0 IOS9 只读
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);

//获取最大触摸压力值
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);

@end

需要熟记的就是两个获取UITouch事件相对于UIView的方法。
举一个拖拽的例子:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    // 想让控件随着手指移动而移动,监听手指移动
    // 获取UITouch对象
    UITouch *touch = [touches anyObject];
    // 获取当前点的位置
    CGPoint curP = [touch locationInView:self];
    // 获取上一个点的位置
    CGPoint preP = [touch previousLocationInView:self];
    // 获取它们x轴的偏移量,每次都是相对上一次
    CGFloat offsetX = curP.x - preP.x;
    // 获取y轴的偏移量
    CGFloat offsetY = curP.y - preP.y;
    // 修改控件的形变或者frame,center,就可以控制控件的位置
    // 形变也是相对上一次形变(平移)
    // CGAffineTransformMakeTranslation:会把之前形变给清空,重新开始设置形变参数
    // make:相对于最原始的位置形变
    // CGAffineTransform t:相对这个t的形变的基础上再去形变
    // 如果相对哪个形变再次形变,就传入它的形变
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
    
}

UIEvent

//事件类型
typedef NS_ENUM(NSInteger, UIEventType) {
    UIEventTypeTouches,//触摸事件(通过触摸、手势进行触发,例如手指点击、缩放)
    UIEventTypeMotion,//运动事件,通过加速器进行触发(例如手机晃动)
    UIEventTypeRemoteControl,//远程控制事件通过其他远程设备触发(例如耳机控制按钮)
};

 // 触摸事件的类型
typedef NS_ENUM(NSInteger, UIEventSubtype) {
    
    UIEventSubtypeNone                              = 0,
    //摇晃 
    UIEventSubtypeMotionShake                       = 1,
   //播放
    UIEventSubtypeRemoteControlPlay                 = 100,
   //暂停
    UIEventSubtypeRemoteControlPause                = 101,
    //停止
    UIEventSubtypeRemoteControlStop                 = 102,
    //播放和暂停切换 
    UIEventSubtypeRemoteControlTogglePlayPause      = 103,
    //下一首
    UIEventSubtypeRemoteControlNextTrack            = 104,
    //上一首
    UIEventSubtypeRemoteControlPreviousTrack        = 105,
     //开始后退 
    UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
    //结束后退 
    UIEventSubtypeRemoteControlEndSeekingBackward   = 107,
    //开始快进 
    UIEventSubtypeRemoteControlBeginSeekingForward  = 108,
    //结束快进
    UIEventSubtypeRemoteControlEndSeekingForward    = 109,
};


NS_CLASS_AVAILABLE_IOS(2_0) @interface UIEvent : NSObject
//事件类型
@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);
// 触摸事件的类型
@property(nonatomic,readonly) UIEventSubtype  subtype NS_AVAILABLE_IOS(3_0);

//事件的时间戳
@property(nonatomic,readonly) NSTimeInterval  timestamp;

//所有的触摸 
- (nullable NSSet <UITouch *> *)allTouches;
//获得UIWindow的触摸
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
//获得UIView的触摸  
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
//获得事件中特定手势的触摸
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS(3_2);

//会将丢失的触摸放到一个新的 UIEvent 数组中,你可以用 coalescedTouchesForTouch(_:) 方法来访问
- (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);
//辅助UITouch的触摸,预测发生了一系列主要的触摸事件。这些预测可能不完全匹配的触摸的真正的行为,因为它的移动,所以他们应该被解释为一个估计。
- (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);

@end


Touch Event、UIControl、UIGestureRecognizer三兄弟的恩怨情仇

《iOS基础补完计划--透过堆栈看事件响应机制》


最后

本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果愿意补充以及不吝赐教小弟会更加感激。


参考资料

官方文档 - UIResponder
解析iOS开发中的FirstResponder第一响应对象
UIKit: UIResponder
iOS事件传递
iOS触摸事件详解
你真的了解UIEvent、UITouch吗?
iOS开发中的NSUndoManager的undo/redo功能(一颗后悔药)
iOS8新特性扩展(Extension)应用之四——自定义键盘控件
深入讲解iOS键盘三:自定义键盘的两种方法
iOS开发inputView和inputAccessoryView
谈UITextView、UITextField的InPutView和AccessoryInputView的便利
iOS自定义键盘切换效果
textInputMode
iOS Search API - NSUserActivity
iOS点击事件和手势冲突
史上最详细的iOS之事件的传递和响应机制-原理篇
iOS触摸事件全家桶

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343