UIResponder
响应和处理事件的 abstract interface。(UIResponder 是一个极重要的继承自 NSObject 的抽象接口,它几乎是 UIKit 框架下所有 UI 类的父类(也有特殊,如 UIImage 类则是直接继承自 NSObject),正是从它建立了 NSObject 和 UI 层之间的连接。)
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIResponder : NSObject <UIResponderStandardEditActions>
响应者对象(即 UIResponder 实例)构成 UIKit 应用程序的事件处理主干(event-handling backbone)。许多关键对象也是响应者,包括 UIApplication 对象、UIViewController 对象和所有 UIView 对象(包括 UIWindow)。当事件发生时,UIKit 会将它们调度给应用程序的响应者对象进行处理。
有几种事件,包括触摸事件(touch events)、运动事件(motion events)、遥控事件(remote-control events)和按键事件(press events)。要处理特定类型的事件,响应者必须重写相应的方法。例如,为了处理触摸事件,响应者实现 touchesBegan:withEvent:、touchesMoved:withEvent:、touchesEnded:withEvent: 和 touchesCancelled:withEvent: 方法。在触摸的情况下,响应者使用 UIKit 提供的事件信息(UIEvent)来跟踪这些触摸的变化,并适当地更新应用程序的界面。
除了处理事件外,UIKit 响应者还管理将未处理的事件转发到应用程序的其他部分。如果给定的响应者不能处理事件,它会将该事件转发给响应者链中的下一个响应者(文档应该错了,文档写的是 "next event")。UIKit 动态管理响应者链,使用预定义的规则来确定下一个接收事件的响应者对象。例如,view 将事件转发到其 superview,层次结构的 root view 将事件转发到其 view controller。
响应者处理 UIEvent 对象,但也可以通过 input view 接受 custom input view。系统的键盘是 input view 最明显的例子。当用户在屏幕上点击 UITextField 和 UITextView 对象时,此 view 将成为第一个响应者,并显示其 input view,即系统键盘。类似地,你可以创建 custom input views,并在其他响应者激活时显示它们。要将 custom input view 与响应者关联,请将此 view 指定给响应者的 inputView 属性。
有关响应者和响应者链的信息,请参见 Using Responders and the Responder Chain to Handle Events。(后面我们会详细学习该文档)
Managing the Responder Chain(管理响应者链)
nextResponder
返回响应者链中的下一个响应者,如果没有下一个响应者,则返回 nil。
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
Return Value: 响应者链中的下一个对象;如果这是链中的最后一个对象,则为 nil。
UIResponder 类不会自动存储或设置下一个响应者,因此此方法在默认情况下返回 nil。子类必须重写此方法并返回适当的下一个响应者。例如,UIView 实现此方法并返回管理它的 UIViewController 对象(如果有)或它的 superview 对象(如果没有 UIViewController)。UIViewController 类似地实现该方法并返回其 view 的 superview(测试的是返回 null)。UIWindow 返回应用程序对象(application)(或 UIWindowScene)。共享 UIApplication 对象(shared UIApplication object)通常返回 nil,但如果该对象是 UIResponder 的子类并且尚未被调用来处理事件,则返回其 app delegate。
isFirstResponder
返回一个布尔值,指示此对象是否是第一响应者。
@property(nonatomic, readonly) BOOL isFirstResponder;
Return Value: 如果 receiver 是第一响应者,则为 YES;否则,则为 NO。
UIKit 最初将一些类型的事件(例如运动事件 UIEventTypeMotion)调度给第一响应者。
canBecomeFirstResponder
返回一个布尔值,指示此对象是否可以成为第一响应者。
@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO
Return Value: 如果 receiver 可以成为第一响应者,则为 YES,否则为 NO。
默认情况下,此方法返回 NO。子类必须重写此方法并返回 YES 才能成为第一响应者。不要在当前不在活动视图层次结构中的 view 上调用此方法。结果是不确定的。
becomeFirstResponder
要求 UIKit 使该对象成为其 window 中的第一响应者。
- (BOOL)becomeFirstResponder;
Return Value: 如果此对象现在是第一响应者,则为 YES;否则,则为 NO。
如果希望当前对象成为第一响应者,请调用此方法。调用此方法并不能保证对象将成为第一个响应者。UIKit 要求当前的第一响应者辞去第一响应者的职务,但它可能不会。如果是这样,UIKit 将调用该对象的 canBecomeFirstResponder 方法,默认情况下该方法返回 NO。如果此对象成功成为第一响应者,则以第一响应者为目标的后续事件将首先传递到此对象,UIKit 将尝试显示对象的 input view(如果有)。
切勿对不属于活动视图层次结构的 view 调用此方法。你可以通过检查其 window 属性来确定 view 是否在屏幕上。如果该属性包含有效 window,则它是活动视图层次结构的一部分。如果该属性为 nil,则该 view 不是有效视图层次结构的一部分。
你可以在自定义响应程序中重写此方法,以更新对象的状态或执行某些操作,例如突出显示所选内容。如果重写此方法,则必须在实现中的某个点调用super。
canResignFirstResponder
返回一个布尔值,指示 receiver 是否愿意放弃其第一响应者的身份。
@property(nonatomic, readonly) BOOL canResignFirstResponder; // default is YES
Return Value: 如果 receiver 可以放弃其第一响应者的身份,则为 YES,否则为 NO。
默认情况下,此方法返回 YES。你可以在自定义响应程序中重写此方法,并在需要时返回其他值。例如,包含无效内容的 text field(UITextField)可能想要返回 NO,以确保用户首先更正该内容。
resignFirstResponder
通知此对象已被要求在其 window 中放弃其作为第一响应者的身份。
- (BOOL)resignFirstResponder;
默认实现返回 YES,即放弃第一响应者身份。你可以在自定义响应程序中重写此方法,以更新对象的状态或执行其他操作,例如从选择中删除突出显示。你也可以返回 NO,拒绝放弃第一响应者身份。如果重写此方法,则必须在代码中的某个点调用 super(超类实现)。(想起 UITextField 调用 resignFirstResponder 函数,隐藏系统键盘,大概是放弃第一响应者身份时同时隐藏其关联的 input view)
Responding to Touch Events(响应触摸事件)
通常,所有执行自定义触摸处理的响应者都应重写所有这四种方法。对于每个正在处理的 touch(你在 touchesBegan:withEvent: 中收到的 touch),你的响应者将收到 touchesEnded:withEvent: 或 touchesCancelled:withEvent:。你必须处理已取消的触摸(touchesCancelled:withEvent:),以确保应用程序中的正确行为。否则很可能导致不正确的行为或崩溃。
touchesBegan:withEvent:
告诉该对象 view 或 window 中发生了一个或多个 new touches。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
touches: 一组 UITouch 实例,表示事件开始阶段的 touch,由事件表示。对于 view 中的 touch,默认情况下,此集合仅包含一个 touch。要接收多次 touch,必须将 view 的 multipleTouchEnabled 属性设置为 YES。event: touchs 所属的事件。
当在 view 或 window 中检测到 new touch 时,UIKit 会调用此方法。许多 UIKit 类重写此方法并使用它来处理相应的 touch event。此方法的默认实现将消息转发到响应者链上。在创建自己的子类时,调用 super 来转发你自己没有处理的任何事件。例如,
[super touchesBegan:touches withEvent:event];
如果你在不调用 super 的情况下重写此方法(一种常用模式),那么你还必须重写处理 touch event 的其他方法,即使你的实现什么都不做。
touchesMoved:withEvent:
当与事件相关联的一个或多个 touches 发生更改时,通知响应者。
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
touches: 一组 UITouch 实例,表示其值已更改的 touch。这些 touch 都属于指定的事件。对于 view 中的 touch,默认情况下,此集合仅包含一个 touch。要接收多个 touch,必须将 view 的 multipleTouchEnabled 属性设置为 YES。event: touchs 所属的事件。
当 touch 的 location 或 force 发生变化时,UIKit 调用此方法。许多 UIKit 类重写此方法并使用它来处理相应的 touch event。此方法的默认实现将消息转发到响应者链上。在创建自己的子类时,调用 super 来转发你自己没有处理的任何事件。例如,
[super touchesMoved:touches withEvent:event];
如果你在不调用 super 的情况下重写此方法(一种常用模式),那么你还必须重写处理 touch event 的其他方法,即使你的实现什么都不做。
touchesEnded:withEvent:
当一个或多个手指从 view 或 window 抬起时,通知响应者。
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
touches: 一组 UITouch 实例,表示由 event 表示的事件的结束阶段的 touch。对于视图中的触摸,默认情况下,此集合仅包含一个 touch。要接收多个 touch,必须将 view 的 multipleTouchEnabled 属性设置为 YES。event: touchs 所属的事件。
当手指或 Apple Pencil 不再接触屏幕时,UIKit 调用此方法。许多 UIKit 类重写此方法,并使用它来清除处理相应触摸事件所涉及的状态。此方法的默认实现将消息转发到响应者链上。在创建自己的子类时,调用 super 来转发你自己没有处理的任何事件。例如,
[super touchesEnded:touches withEvent:event];
如果你在不调用 super 的情况下重写此方法(一种常用模式),那么你还必须重写处理 touch event 的其他方法,即使你的实现什么都不做。
touchesCancelled:withEvent:
当系统事件取消触摸序列时通知响应者(例如系统警报、手机来电)。
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
touches: 一组 UITouch 实例,表示由 event 表示的事件的结束阶段的 touch。对于视图中的触摸,默认情况下,此集合仅包含一个 touch。要接收多个 touch,必须将 view 的 multipleTouchEnabled 属性设置为 YES。event: touchs 所属的事件。
UIKit 在接收到需要取消触摸序列的系统中断时调用此方法。中断是指导致应用程序处于非活动状态或导致处理触摸事件的视图从其窗口中移除的任何内容。此方法的实现应该清除与处理触摸序列相关的任何状态。此方法的默认实现将消息转发到响应者链上。在创建自己的子类时,调用 super 来转发你自己没有处理的任何事件。例如,
[super touchesCancelled:touches withEvent:event];
如果在不调用 super 的情况下重写此方法(一种常用模式),则还必须重写处理 touch event 的其他方法(如果仅作为 stub(empty)实现)。
touchesEstimatedPropertiesUpdated:
告诉响应者已收到先前估计的属性的更新值,或者不再期望进行更新。
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches API_AVAILABLE(ios(9.1));
touches: 包含更新属性的 UITouch 对象数组。在每个触摸对象中,UIKit 通过删除每个已更新属性的位标志来更新 estimatedPropertiesExpectingUpdates 属性。
当 UIKit 无法报告触摸的实际值时,它会传递值的估计值,并在 UITouch 对象的 estimatedProperties 和 estimatedPropertiesExpectingUpdates 属性中设置适当的位。当接收到 estimatedPropertiesExpectingUpdate 属性中项目的更新时,UIKit 调用此方法来传递这些更新。如果不再需要一个或多个更新,UIKit 也会调用此方法。使用此方法可以使用 UIKit 提供的新值更新应用程序的内部数据结构。
在实现此方法时,请在 touchs 参数中使用 UITouch 对象的 estimationUpdateIndex 属性来定位应用程序中的原始数据。定位数据后,将来自触摸对象的新值应用于该数据。你可以通过检查触摸对象的 estimatedPropertiesExpectingUpdates 位掩码来确定更新了哪些触摸属性;位掩码中不再包括更新的属性。
由于硬件方面的考虑,与触摸相关的属性可能仍然是估计的。例如,当 Apple Pencil 靠近屏幕边缘时,传感器可能无法确定它的确切高度或方位角。在这些情况下,estimatedProperties 属性继续存储其值仅为估计值的属性列表。
Responding to Motion Events(响应运动事件)
motionBegan:withEvent:
告诉 receiver 运动事件已经开始。
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));
motion: 一个事件子类型常量,指示运动的类型。常见的运动是晃动,由 UIEventSubtypeMotionShake 指示。event: 表示与运动关联的 UIEvent 的对象。
UIKit 仅在运动事件开始和结束时通知响应者。它不报告中间晃动(UIEventSubtypeMotionShake)。运动事件最初被传递给第一响应者,并根据需要被转发到响应者链上。此方法的默认实现将消息转发到响应者链。
motionEnded:withEvent:
告诉 receiver 运动事件已经结束。
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));
motion: 一个事件子类型常量,指示运动的类型。常见的运动是晃动,由 UIEventSubtypeMotionShake 指示。event: 表示与运动关联的 UIEvent 的对象。
UIKit 仅在运动事件开始和结束时通知响应者。它不报告中间晃动(UIEventSubtypeMotionShake)。运动事件最初被传递给第一响应者,并根据需要被转发到响应者链上。此方法的默认实现将消息转发到响应者链。
motionCancelled:withEvent:
告诉 receiver 运动事件已被取消。
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));
motion: 一个事件子类型常量,指示运动的类型。常见的运动是晃动,由 UIEventSubtypeMotionShake 指示。event: 表示与运动关联的 UIEvent 的对象。
UIKit 在接收到需要取消运动事件的中断时调用此方法。中断是指导致应用程序处于非活动状态或导致处理运动事件的 view 从其 window 中移除的任何内容。如果 shaking 持续时间过长,UIKit 也可能调用此方法。所有处理运动事件的响应者都应该实现此方法。在实现中,清除与处理运动事件相关的所有状态信息。(即运动事件被打断时,我们需要做必要的收尾工作)
此方法的默认实现将消息转发到响应者链。
Responding to Press Events(响应 Press 事件)
通常,所有执行自定义 press 处理的响应者都应重写所有这四种方法。
对于每个正在处理的 press,你的响应者将收到 pressesEnded:withEvent: 或 pressesCancelled:withEvent:(在pressesBegan:withEvent:中收到的印刷机)。pressesChanged:withEvent: 将被提供模拟值的 presses 调用(如指尖或模拟按钮)。
你必须处理取消的 presses,以确保应用程序中的正确行为。否则很可能导致不正确的行为或崩溃。
pressesBegan:withEvent:
第一次按下物理按钮时告诉该对象。
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
presses: 一组 UIPress 实例,代表发生的 new presses。每次按下的阶段都设置为 UIPressPhaseBegan。event: presses 所属的事件。
用户按下新按钮时,UIKit 会调用此方法。使用此方法可以确定按下了哪个按钮并采取了所需的操作。
此方法的默认实现将消息转发到响应者链。创建自己的子类时,请调用 super 来转发你自己无法处理的所有事件。
pressesChanged:withEvent:
当与 press 关联的值发生更改时告诉该对象。
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
presses: 一组包含更改值的 UIPress 实例。event: presses 所属的事件。
当与按钮或指尖关联的模拟值更改时,UIKit 会调用此方法。例如,当按钮的模拟力值改变时,它将调用此方法。使用此方法可以采取任何必要的措施来响应更改。
此方法的默认实现将消息转发到响应者链。创建自己的子类时,请调用 super 来转发你自己无法处理的所有事件。
pressesEnded:withEvent:
告诉对象何时释放按钮。
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
presses: 一组 UIPress 实例,代表用户不再按下的按钮。每次按下的阶段都设置为 UIPressPhaseEnded。event: presses 所属的事件。
当用户停止按一个或多个按钮时,UIKit 调用此方法。使用此方法可以采取任何必要的操作来响应 press 的结束。此方法的默认实现将消息转发到响应者链上。在创建自己的子类时,调用 super 来转发你自己没有处理的任何事件。
pressesCancelled:withEvent:
当系统事件取消按下事件时告诉该对象(例如内存不足警告)。
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
presses: 一组 UIPress 实例,代表与事件关联的 presses。每次 press 的阶段都设置为 UIPressPhaseCancelled。event: presses 所属的事件。
当 UIKit 接收到需要取消按下顺序的系统中断时,它调用此方法。中断是指导致应用程序处于非活动状态或导致处理 press 事件的 view 从其 window 中删除的任何内容。此方法的实现应该清除与处理 press 序列相关的任何状态。未能处理取消可能会导致不正确的行为或崩溃。
此方法的默认实现将消息转发到响应者链。创建自己的子类时,请调用 super 来转发你自己无法处理的所有事件。
Responding to Remote-Control Events(响应远程控制事件)
在接收到远程控制事件时通知对象。
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event API_AVAILABLE(ios(4.0));
event: 封装了远程控制命令的事件对象。远程控制事件具有 UIEventTypeRemoteControl 的类型。
远程控制事件源于来自外部附件(包括耳机)的命令。应用程序通过控制呈现给用户的音频或视频媒体来响应这些命令。接收响应程序对象应该检查事件的子类型,以确定预期的命令(例如,play(UIEventSubtypeRemoteControlPlay)),然后相应地继续。
要允许传递远程控制事件,必须调用 UIApplication 的 beginReceivingRemoteControlEvents 方法。要关闭远程控制事件的传递,请调用 endReceivingRemoteControlEvents 方法。
Managing Input Views(管理输入视图)
inputView
当 receiver 成为第一响应者时显示的自定义输入视图。
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputView API_AVAILABLE(ios(3.2));
此属性通常用于提供一个视图,以替换为 UITextField 和 UITextView 对象显示的系统提供的键盘。
此只读属性的值为 nil。需要自定义视图来收集用户输入的响应者对象应将此属性重新声明为可读写,并使用它来管理其自定义输入视图。当 receiver 成为第一响应者时,响应者基础结构会自动显示指定的输入视图。类似地,当 receiver 退出其第一响应者状态时,响应者基础结构会自动关闭指定的输入视图。
inputViewController
receiver 成为第一响应者时要使用的 custom input view controller。
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputViewController API_AVAILABLE(ios(8.0));
此属性通常用于提供视图控制器,以替换为 UITextField 和 UITextView 对象显示的系统提供的键盘。
此只读属性的值为 nil。如果要提供自定义输入视图控制器以替换应用程序中的系统键盘,请在 UIResponder 子类中将此属性重新声明为读写。然后可以使用此属性管理自定义输入视图控制器。当接收者成为第一个响应者时,响应者基础结构自动呈现指定的输入视图控制器。类似地,当接收器放弃其第一响应程序状态时,响应程序基础结构会自动取消指定的输入视图控制器。
inputAccessoryView
当 receiver 成为第一响应者时显示的自定义输入附件视图(custom input accessory view)。
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputAccessoryView API_AVAILABLE(ios(3.2));
此属性通常用于将附件视图附加到为 UITextField 和 UITextView 对象显示的系统提供的键盘上。(位置是在键盘的顶部)
此只读属性的值为 nil。如果要将自定义控件附加到系统提供的输入视图(如系统键盘)或自定义输入视图(在 inputView 属性中提供的视图),请在 UIResponder 子类中将此属性重新声明为读写。然后可以使用此属性管理自定义附件视图。当 receiver 成为第一个响应者时,响应者基础结构会在显示附件视图之前将其附加到适当的输入视图。
inputAccessoryViewController
自定义输入附件视图控制器,以在 receiver 成为第一响应者时显示。
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputAccessoryViewController API_AVAILABLE(ios(8.0));
reloadInputViews
当对象是第一响应者时,更新自定义输入和附件视图(custom input and accessory views)。(如果在对象是第一响应者的情况下调用,则重新加载 inputView、inputAccessoryView 和 textInputMode。否则忽略。)
- (void)reloadInputViews API_AVAILABLE(ios(3.2));
当它是第一响应者时,可以使用此方法刷新与当前对象关联的自定义输入视图(custom input view)或输入附件视图( input accessory view)。视图将立即替换(即不会附加动画)。如果当前对象不是第一响应者,则此方法无效。
Getting the Undo Manager()
undoManager
返回响应者链中最近的共享撤消(shared undo)管理器。
@property(nullable, nonatomic,readonly) NSUndoManager *undoManager API_AVAILABLE(ios(3.0));
默认情况下,应用程序的每个 window 都有一个撤消管理器:一个用于管理撤消和重做操作的共享对象。但是,响应程序链中任何对象的类都可以有自己的自定义撤消管理器。(例如,UITextField的实例有自己的 undoManager,当文本字段退出第一响应者状态时,该管理器将被清除。)当你请求 undoManager 时,请求将进入响应者链,UIWindow 对象将返回一个可用的实例。
你可以将撤消管理器添加到视图控制器,以执行托管视图本地的撤消和重做操作。 ...
UIResponder 的文档就看到这吧,我们最需要记住的就是当我们需要自己处理事件时需要重写 touches...(响应触摸事件)、presses...(响应按键事件)、motion...(响应运动事件) 系列函数,以及 nextResponder 属性,是它链接了响应者链。
下面我们阅读 Responder object 文档,虽然它被标记为过时,但是还是有一定的参考意义。
Responder object
响应者是可以响应事件并处理它们的对象。所有响应者对象都是最终从 UIResponder(iOS)或 NSResponder(OS X)继承的类的实例。这些类声明了一个用于事件处理的编程接口,并定义了响应者的默认行为。应用程序的可见对象几乎总是响应者(例如,windows、views 和 controls),而应用程序对象(AppDelegate)也是响应者。在 iOS 中,视图控制器(UIViewController 对象)也是响应者对象。
若要接收事件,响应者必须实现适当的事件处理方法,在某些情况下,告诉应用它可以成为第一响应者。
The First Responder Receives Some Events First(第一响应者首先接收一些事件)
在应用程序中,首先接收多种事件的响应者对象称为第一响应者。它接收关键事件、运动事件和 action 消息等。(鼠标事件和多点触控事件首先转到鼠标指针或手指下的视图;该视图可能是也可能不是第一响应者。)第一响应者通常是应用程序认为最适合处理事件的窗口中的视图。为了接收事件,响应者还必须表明其成为第一响应者的意愿;对于每个平台,响应者以不同的方式进行:
// OS X
- (BOOL)acceptsFirstResponder {
return YES;
}
// iOS
- (BOOL)canBecomeFirstResponder {
return YES;
}
除了接收事件消息外,响应者还可以接收未指定 target 的 action 消息。(action 消息由 buttons 和 controls 等控件在用户操作时发送。)
The Responder Chain Enables Cooperative Event Handling(响应者链支持协作事件处理)
如果第一个响应者无法处理事件或 action 消息,它会将其转发给一个称为响应者链的链接系列中的 “下一个响应者” (next responder)。响应者链允许响应者对象将处理事件或 action 消息的责任转移到应用程序中的其他对象。如果响应者链中的对象无法处理事件或 action,它会将消息传递给链中的下一个响应者。消息沿着链向上传播,指向更高级别的对象,直到被处理为止。如果未处理,应用程序将丢弃它。
The responder chain for iOS (left) and OS X (right)
The path of an event。事件在响应者链上的一般路径从第一个响应者的视图或鼠标指针或手指下的视图开始。从那里开始,它向上进入视图层次结构,进入 window 对象,然后进入全局应用程序对象。但是,iOS 中事件的响应者链为该路径添加了一个变体:如果 view 由 view controller 管理,并且 view 无法处理事件,则 view controller 将成为下一个响应者。
The path of an action message。对于 action 消息,OS X 和 iOS 都将响应者链扩展到其他对象。在 OS X 中,对于基于文档体系结构的应用程序、使用窗口控制器的应用程序(NSWindowController)和不适合这两个类别的应用程序,action 消息的响应者链是不同的。此外,如果 OS X 上的应用程序同时具有一个 key window 和一个 main window,那么 action 消息所经过的响应者链可能涉及两个窗口的视图层次结构。
接着下面是 Handling Touches in Your View(处理视图中的触摸)文档。
Handling Touches in Your View(处理视图中的触摸)
如果触摸处理(touch handling)与 view 的内容有复杂的链接(intricately linked),则直接在 view 子类上使用触摸事件(touch events)。
如果你不打算对自定义视图使用手势识别器,则可以直接从视图本身处理触摸事件。因为 view 是响应者,所以它们可以处理多点触控事件和许多其他类型的事件。当 UIKit 确定某个 view 中发生了触摸事件时,它将调用该 view 的 touchesBegan:withEvent:、touchesMoved:withEvent: 或 touchesEnded:withEvent: 方法。你可以在自定义视图中重写这些方法,并使用它们提供对触摸事件的响应。(来自 UIResponder)
你在视图(或任何响应者)中重写的处理触摸的方法对应于触摸事件处理过程的不同阶段。例如,图1 说明了触摸事件的不同阶段。当手指(或 Apple Pencil)接触屏幕时,UIKit 会创建 UITouch 对象,将触摸位置设置为适当的点,并将其 phase 属性设置为 UITouchPhaseBegan。当同一个手指在屏幕上移动时,UIKit 会更新触摸位置,并将触摸对象的 phase 属性更改为 UITouchPhaseMoved。当用户将手指从屏幕上提起时,UIKit 将 phase 属性更改为 UITouchPhaseEnded,触摸序列结束。
Figure 1 The phases of a touch event(触摸事件的各个阶段)
类似地,系统可以随时取消正在进行的触摸序列;例如,当来电中断应用程序时。当它这样做时,UIKit 通过调用 touchs 来通知你的 view 的 touchesCancelled:withEvent: 方法。你可以使用该方法对 view 的数据结构执行任何必要的清理。
UIKit 为触摸屏幕的每个新手指创建一个新的 UITouch 对象。触摸本身是通过当前 UIEvent 对象传递的。UIKit 区分了来自手指和 Apple Pencil 的触摸,你可以对它们进行不同的处理。
Important: 在其默认配置中,view 仅接收与事件关联的第一个 UITouch 对象,即使有多个手指接触 view 也是如此。要接收额外的触摸,必须将 view 的 multipleTouchEnabled 属性设置为 true。也可以使用属性检查器在 Interface Builder 中配置此属性。
接着下面是 Using Responders and the Responder Chain to Handle Events(使用响应者和响应者链来处理事件)文档。
Using Responders and the Responder Chain to Handle Events(使用响应者和响应者链来处理事件)
了解如何处理通过你的应用传播的事件。
应用程序使用响应者对象(responder objects)接收和处理事件。responder 对象是 UIResponder 类的任何实例,常见的子类包括 UIView、UIViewController 和 UIApplication。响应者接收原始事件数据,并且必须处理该事件或将其转发给另一个响应者对象。当应用程序接收到事件时,UIKit 会自动将该事件定向到最合适的响应者对象(称为第一响应者)。
未处理的事件在活动响应者链中从一个响应者传递到另一个响应者,这是应用程序响应者对象的动态配置。Figure 1 显示了应用程序中的响应者,其界面包含一个 label、一个 text field、一个 button 和两个背景 views。该图还显示了事件如何沿着响应者链从一个响应者移动到下一个响应者。
Figure 1 Responder chains in an app(应用中的响应者链)
如果 text field 不处理事件,UIKit 会将事件发送到 text field 的父 view 对象,后跟 window 的根视图。在将事件定向到 window 之前,响应者链从根视图转移到拥有的视图控制器。如果 window 无法处理事件,UIKit 会将事件传递给 UIApplication 对象,如果该对象是 UIResponder 的实例而不是响应程序链的一部分,则可能会传递给 app delegate。
Determining an Event's First Responder(确定事件的第一响应者)
UIKit 根据事件的类型将对象指定为事件的第一响应者。事件类型包括:
| Event type | First responder |
|---|---|
| Touch events | The view in which the touch occurred.(发生触摸的视图) |
| Press events | The object that has focus. |
| Shake-motion events | The object that you (or UIKit) designate. |
| Remote-control events | The object that you (or UIKit) designate. |
| Editing menu messages | The object that you (or UIKit) designate. |
Note: 与加速计(accelerometers)、陀螺仪(gyroscopes)和磁力计(magnetometer)相关的运动事件不遵循响应链。相反,Core Motion 将这些事件直接传递给指定的对象。有关详细信息,请参见 Core Motion Framework
Controls 使用 action 消息直接与其关联的 target 对象通信。当用户与 control 交互时,control 将向其 target 对象发送 action 消息。Action messages 不是事件,但它们仍然可以利用响应者链。当 control 的 target 对象为 nil 时,UIKit 从目标对象开始遍历响应者链,直到找到实现适当 action 方法的对象。例如,UIKit 编辑菜单(editing menu)使用此行为来搜索响应者对象,这些对象实现了名称为 cut:、copy: 或 paste: 的方法。
Gesture recognizers 在 view 之前接收 touch 和 press 事件。如果 view 的 gesture recognizers 无法识别一系列 touches,UIKit 会将 touches 发送到 view。如果 view 不能处理 touches,UIKit 会将它们向上传递到响应者链。有关使用 gesture recognizer 处理事件的详细信息,请参阅 Handling UIKit Gestures(下面有其翻译)。
Determining Which Responder Contained a Touch Event(确定哪个响应者包含触摸事件)
UIKit 使用基于 view 的 Hit-Testing(view-based hit-testing)来确定触摸事件发生的位置。具体来说,UIKit 将触摸位置与 view 层次结构中 view 对象的 bounds 进行比较。UIView 的 hitTest:withEvent: 方法遍历视图层次结构,查找包含指定触摸的最深子视图,该子视图将成为触摸事件的第一响应者。(会直接把 UIEvent 交给它处理)
Note: 如果触摸位置在视图 bounds 之外,则 hitTest:withEvent: 方法忽略该视图及其所有子视图。因此,当视图的 clipsToBounds 属性为 NO 时,即使超出该视图 bounds 的子视图恰好包含触摸,也不会返回这些子视图。有关 Hit-Testing 行为的更多信息,请参阅 UIView 的 hitTest:withEvent: 方法(上面我们已经详细分析过了)。
当触摸发生时,UIKit 创建一个 UITouch 对象并将其与 view 相关联。当触摸位置或其他参数改变时,UIKit 用新信息更新同一 UITouch 对象。唯一不变的属性是 view。(即使触摸位置移动到原始 view 之外,触摸 view 属性中的值也不会更改。(如我们常见的页面上有一个小的滚动区域时,我们手指先摸到它并滑动该小区域开始滚动,当我们的手指超出此块滚动区域并不离开屏幕时,此小滚动区域也一直响应我们手指的滑动))当触摸结束时,UIKit 释放 UITouch 对象。
Altering the Responder Chain(改变响应者链)
你可以通过重写响应者对象的 nextResponder 属性来更改响应者链。当你这样做时,下一个响应者就是你返回的对象。
许多 UIKit 类已经重写此属性并返回特定的对象,包括:
- UIView 对象。如果 view 是 view controller 的 root view,则下一个响应者是 view controller;否则,下一个响应者是 view 的 superview。
- UIViewController 对象。
- 如果 view controller 的 view 是 window 的 root view,则它的下一个响应者是 window 对象。
- 如果 view controller 由另一个 view controller 呈现,则它的下一个响应者是呈现 view controller。(parent view controller)
- UIWindow 对象。window 的下一个响应者是 UIApplication 对象。
- UIApplication 对象。下一个响应者是 app delegate,但仅当该 app delegate 是 UIResponder 的实例并且不是 view、view controller 或 app 对象本身时,才是下一个响应者。
事件处理、响应者以及响应者链的相关内容到这里就可以结束了,下面我们再对 gesture recognizers(继承自 NSObject,响应事件的方式同 UIControl,也是 target-action 机制) 和 target-action 进行一个拓展学习,它们还挺重要的。(当一个 view 同时实现了 touches... 系列函数和添加了手势时,我们去触摸该 view 首先会调用 touchesBegan:withEvent: 函数,而当 gesture recognizers 识别出手势后会调用 view 的 touchesCancelled:withEvent: 打断 touches... 系列函数的执行,然后去执行手势的 action 函数。)
Handling UIKit Gestures(处理 UIKit 手势)
使用 gesture recognizers 简化 touch handling 并创建一致的用户体验。
Gesture recognizers 是处理视图中的 touch(UIEventTypeTouches 屏幕上的触摸事件) 或 press (UIEventTypePresses 设备的物理按钮)事件的最简单方法。可以将一个或多个 gesture recognizers 附加到任何视图。Gesture recognizers 封装了为该视图处理和解释传入事件所需的所有逻辑,并将它们与已知模式(手势类型)相匹配。当检测到匹配时,gesture recognizer 会通知其指定的目标对象,该目标对象可以是 view controller、view 本身或应用程序中的任何其他对象。
Gesture recognizers 使用目标动作设计模式(target-action design pattern)发送通知。 当 UITapGestureRecognizer 对象在视图中检测到单个手指点击时,它将调用 view 的 view controller 的操作方法,你可以使用该方法提供响应。
Figure 1 Gesture recognizer notifying its target(手势识别器通知目标)
Gesture recognizers 有两种类型:离散(discrete)和连续(continuous)。一个离散的手势识别器(discrete gesture recognizer)会在手势被识别后准确地调用你的动作方法(action method)一次。满足初始识别条件后,连续手势识别器(continuous gesture recognizer)会多次执行对动作方法的调用,并在手势事件中的信息发生更改时通知你。例如,每次触摸位置更改时,UIPanGestureRecognizer 对象都会调用动作方法。
Interface Builder 包含每个标准 UIKit gesture recognizers 的对象。它还包括一个自定义手势识别器对象,你可以使用它来表示自定义 UIGestureRecognizer 子类。
Configuring a gesture recognizer(配置手势识别器)
要配置手势识别器:
- 在 storyboard 中,将手势识别器拖到视图中。
- 实现识别手势时要调用的动作方法;参见 Listing 1。
- 将你的动作方法连接到手势识别器。
通过右键单击手势识别器并将其 Sent Action selector 连接到界面中的相应对象,可以在 Interface Builder 中创建此连接。你还可以使用手势识别器的 addTarget(_:action:) 方法以编程方式配置 action 方法。
Listing 1 显示了手势识别器的 action 方法的通用格式。如果愿意,可以更改参数类型以匹配特定的手势识别器子类。
Listing 1 Gesture recognizer action methods(手势识别器 action 方法)
// Swift
@IBAction func myActionMethod(_ sender: UIGestureRecognizer) { ... }
// Objective-c
- (IBAction)myActionMethod:(UITapGestureRecognizer *)sender { ... }
Responding to Gestures(响应手势)
与 gesture recognizer 关联的 action method 提供应用程序对该手势的响应。对于离散手势,你的 action method 类似于 button 的 action method。一旦调用了 action method,就可以执行适合该手势的任何任务。对于连续的手势,action method 可以响应对手势的识别,但也可以在识别手势之前跟踪事件。跟踪事件可以让你创建更具交互性的体验。例如,你可以使用 UIPanGestureRecognizer 对象的更新来重新定位应用程序中的内容。(如让一个 imageView 跟着你的手指移动位置)
gesture recognizer 的 state 属性传递对象的当前识别状态。对于连续的手势,gesture recognizer 将从更新此属性的值 UIGestureRecognizer.State.began 至 UIGestureRecognizer.State.changed 至 UIGestureRecognizer.State.ended,或 UIGestureRecognizer.State.cancelled。action methods 使用此属性来确定适当的 action 过程。例如,可以使用 began 和 changed 状态对内容进行临时更改,使用 ended 状态使这些更改永久化,使用 cancelled 状态放弃更改。在执行操作之前,请始终检查 gesture recognizer 的 state 属性值。
手势的文档就看到这里吧,文档里面还有各种类型手势的处理以及实现自定义手势、离散手势、连续手势的实现、手势的状态机等等内容,大家可以根据需要进行学习。
Target-Action
尽管 delegation、bindings 和 notification 对于处理程序中对象之间的某些形式的通信很有用,但它们并不特别适合于最明显的通信类型。典型的应用程序的用户界面由许多图形对象组成,其中最常见的对象可能是控件(controls)。控件是真实世界或逻辑设备(按钮(button)、滑块(slider)、复选框(checkboxes)等)的图形模拟;与真实世界控件(如收音机调谐器)一样,你使用它将你的意图传达给某个系统,而该系统是应用程序的一部分。
控件(control)在用户界面上的作用很简单:它解释用户的意图并指示其他对象执行该请求。当用户通过单击控件或按返回键对控件进行操作时,硬件设备将生成一个原始事件。控件接受事件(为 Cocoa 适当包装),并将其转换为特定于应用程序的指令。然而,事件本身并不能提供很多关于用户意图的信息;它们只是告诉你用户单击了鼠标按钮或按下了一个键。因此,必须借助某种机制来提供事件与指令之间的转换。这种机制被称为 target-action。
Cocoa 使用 target-action 机制在 control 和另一个对象(target)之间进行通信。这种机制允许 control 和 OS X 中的一个或多个单元格封装必要的信息,以便将特定于应用程序的指令发送到适当的对象。接收对象通常是自定义类的实例,称为 target。action 是 control 发送给 target 的消息。对用户事件感兴趣的对象(target)是赋予它意义的对象,这种意义通常反映在它赋予 action 的名称中。
The Target
target 是 action 消息的接收者。control(或更常见的是 cell)将其 action 消息的 target 作为 outlet 保存(请参见 Outlets)。target 通常是你的一个自定义类的实例,尽管它可以是其类实现适当 action 方法的任何 Cocoa 对象。
还可以将 cell 或 control 的 target outlet 设置为 nil,并在运行时确定 target 对象。当 target 为 nil 时,应用程序对象(NSApplication 或 UIApplication)按指定的顺序搜索适当的 receiver:
- 它从 key window 中的第一个响应者开始,然后跟随 nextResponder 链接将响应者链链接到 window 对象(NSWindow 或 UIWindow)的内容视图。
Note: OS X 中的 key window 响应于应用程序的按键(key presses)操作,并且是菜单和对话框中消息的接收者。应用程序的 main window 是用户 actions 的主要焦点,并且通常还具有关键状态。
- 它先尝试 window 对象,然后再尝试 window 对象的 delegate。
- 如果 main window 与 key window 不同,那么它将从 main window 中的第一响应者开始,并沿着 main window 的响应者链向上到达 window 对象及其 delegate。
- 接下来,application 对象尝试响应。如果无法响应,它将尝试其 delegate。application 对象及其 delegate 是最后的 receiver。
Control 不(也不应该)保留其 targets。但是,发送 action 消息的 controls 的 clients(通常是 applications)负责确保其 targets 可用于接收 action 消息。为此,它们可能必须将 targets 保留在内存管理的环境中。此预防措施同样适用于委托(delegates)和数据源(data sources)。
The Action
action 是 control 发送给 target 的消息,或者从 target 的角度来看,是 target 实现的响应 action 消息的方法。control 或(通常是 AppKit 中的情况)control 的 cell 将 action 存储为 SEL 类型的实例变量。SEL 是一种 Objective-C 数据类型,用于指定消息的签名。action 消息必须具有简单、独特的签名。它调用的方法不返回任何内容,通常只有一个 id 类型的参数。按照约定,此参数名为 sender。下面是 NSResponder 类中的一个示例,该类定义了许多 action 方法:
- (void)capitalizeWord:(id)sender;
某些 Cocoa 类声明的 action 方法也可以具有等效的签名:
- (IBAction) deleteRecord:(id)sender;
在这种情况下,IBAction 不会为返回值指定数据类型;不会返回值。IBAction 是一个类型限定符,Interface Builder 在应用程序开发过程中会注意到该类型限定符,将以编程方式添加的 action 与其为项目定义的 action 方法的内部列表同步。
sender 参数通常标识发送 action 消息的 control(尽管它可以是另一个被实际发送方替换的对象)。这背后的想法类似于明信片上的回信地址。如果需要,target 可以向发送者查询更多信息。如果实际的发送对象将另一个对象替换为发送方,则应以相同的方式处理该对象。例如,假设你有一个 text field,并且当用户输入文本时,将在 target 中调用 action 方法 nameEntered ::
- (void)nameEntered:(id) sender {
NSString *name = [sender stringValue];
if (![name isEqualToString:@""]) {
NSMutableArray *names = [self nameList];
[names addObject:name];
[sender setStringValue:@""];
}
}
在这里,响应方法提取文本字段的内容,将字符串添加到作为实例变量缓存的数组中,然后清除该字段。对发送方的其他可能查询包括:向 NSMatrix 对象询问其所选行([sender selectedRow])、向 NSButton 对象询问其状态([sender state])、并向与控件关联的任何单元格询问其标记([[sender cell] tag])标记是数字标识符。
文档下面是 Target-Action in the AppKit Framework 和 Target-Action in UIKit,这里只看一下 Target-Action in UIKit。
Target-Action in UIKit
UIKit framework 还声明并实现一组 control 类;此框架中的 control 类继承自 UIControl 类,后者定义了 iOS 的大多数 target-action mechanism。然而,在 AppKit 和 UIKit 框架如何实现 target-action 方面存在一些根本性的差异。其中一个区别是 UIKit 没有任何真正的 cell 类。UIKit 中的 controls 不依赖其 cells 获取 target 和 action 信息。
两个框架如何实现 target-action 的一个更大的区别在于事件模型的性质(nature of the event model)。在 AppKit 框架中,用户通常使用鼠标和键盘来注册事件以供系统处理。这些事件(如单击按钮)是有限和离散的。因此,AppKit 中的 control 对象通常会将单个物理事件识别为其发送到 target 的 action 的触发器。(在按钮的情况下,这是一个 mouse-up 的事件。)在 iOS 中,用户的手指是事件的始作俑者,而不是鼠标点击、鼠标拖动或物理按键。一次可以有多个手指触摸屏幕上的一个对象,这些触摸甚至可以朝不同的方向进行。
为了说明此多点触控事件模型,UIKit 在 UIControl.h 中声明了一组 control-event 常量,这些常量指定用户可以在 control 上进行的各种物理手势,例如从控件中抬起手指、将手指拖动到控件中以及在文本字段中触碰。你可以配置 control 对象,以便它通过向 target 发送 action 消息来响应一个或多个这些触摸事件。UIKit 中的许多控件类实现为生成某些控件事件;例如,UISlider 类的实例生成 UIControlEventValueChanged 控件事件,你可以使用该事件向 target 对象发送 action 消息。
设置 control,使其通过将 target 和 action 与一个或多个 control 事件关联,向 target 对象发送 action 消息。为此,发送 addTarget:action:forControlEvents: 消息到要指定 target-action pair 的控件。当用户以指定的方式触摸控件时,控件通过 sendAction:to:from:forEvent: 消息将 action 消息转发到全局 UIApplication 对象。与在 AppKit 中一样,全局 application 对象是 action 消息的集中调度点。如果控件为 action 消息指定了nil target,应用程序将查询响应程序链中的对象,直到找到一个愿意处理 action 消息的对象,即实现与 action 选择器对应的方法的对象。
与 AppKit 框架(一种操作方法可能仅具有一个或两个有效签名)相比,UIKit 框架允许三种不同形式的 action selector:
- (void)action
- (void)action:(id)sender
- (void)action:(id)sender forEvent:(UIEvent *)event
要了解有关 UIKit 中 target-action mechanism 的更多信息,请阅读 UIControl Class Reference。
参考链接
参考链接:🔗