iOS上的事件传递:第3部分

我们已经通过iOS中的事件传递系统结束了旅程。 在介绍目标动作模式如何工作以及如何使用某些相同的API沿响应者链发送自定义事件之前,让我们回顾一下到目前为止所涵盖的内容。

在第1部分中,我们研究了UIKit如何通过点击测试处理触摸事件以及手势识别器在系统中的位置。 我们还简要地研究了响应者链如何处理这些触摸事件,包括它们遵循的到达视图和视图控制器层次结构的路径。

在第2部分中,我们介绍了UIResponder中定义的其余事件,以及有关Responder Chain如何运行的更多信息。

目标行动

UIKit大量使用目标动作模式。 它在UIControl中定义。 让我们看一看UIControl标题的代码段。

  NS_CLASS_AVAILABLE_IOS(2_0)@interface UIControl:UIView 

----剪断----

-(void)addTarget:(nullable id)目标动作:(SEL)ControlEvents的动作:(UIControlEvents)controlEvents;
-(void)removeTarget:(nullable id)目标动作:(nullable SEL)针对ControlEvents:(UIControlEvents)controlEvents的动作;

-(NSSet *)allTargets;
-(UIControlEvents)allControlEvents;
-(nullable NSArray *)actionsForTarget:(nullable id)target for ControlEvent:(UIControlEvents)controlEvent;

-(void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
  -(void)sendActionsForControlEvents:(UIControlEvents)controlEvents; 

----剪断----

这些方法定义了目标动作模式。 该模式使我们可以定义要处理的事件,接收事件的目标以及要发送到目标的动作(消息)。

事件和动作的这种分离还使我们可以为一个或多个动作定义多个收件人。 有关UIControl和目标动作模式的更多信息,请查看Apple的UIControl文档。

那么,这与活动交付相适应吗? 当前, UIControl使用带有类似于以下内容的调用跟踪来发送操作:

 框架#0:-[BPXLTableViewCell cellButtonTapped:] 
框架#1:-[UIApplication sendAction:to:from:forEvent:]
框架2:-[UIControl sendAction:to:forEvent:]
框架#3:-[UIControl _sendActionsForEvents:withEvent:]
框架4:-[UIControl touchesEnded:withEvent:]
框架5:_UIGestureEnvironmentSortAndSendDelayedTouches
---剪-

这里重要的API调用是-[UIApplication sendAction:to:from:forEvent] 。 在这种情况下,控件将根据进入控件的事件将适当的操作发送到适当的目标。 为UIControl上的操作设置nil目标,最终-[UIApplication sendAction:to:from:forEvent]调用的to:参数中传递nil 。 这称为无目标操作。

无目标动作

-[UIApplication sendAction:to:from:forEvent]是负责将操作发送到给定目标的方法。 UIControl将其用于控件事件处理。 它还可以用于将任意操作发送到任意对象。 真正的能力来自将nil作为to:参数传递的能力。 这导致动作被发送到响应者链。

但是,您可以发送的操作类型有一个约定。 您可以发送的操作消息必须具有以下签名之一:

  -(无效)动作 
-(无效)操作:(id)发送者
-(无效)action:(id)发件人forEvent:(UIEvent *)事件

有了这些知识,让我们看一下如何在应用程序中使用零目标操作。

在实践中

我们将研究在Objective-C和Swift中实现的同一应用程序。 首先,请克隆GitHub存储库。 该应用程序本身看起来像这样:

如您所见,该应用程序包含一个显示项目列表的表格视图。 每个单元格都有一个按钮,使用户可以更改导航栏的标题。 使用单元上的委托并将视图控制器设置为单元的委托,可以轻松实现这一点。 但这可能会带来问题,因为视图控制器可能不是给定消息的响应者。 如果表视图嵌套了几层,且视图控制器包含在其中,则该单元可能需要在层次结构上一级或什至两级发送操作。 这是一个非常简单的单元实现:

  @实现BPXLTableViewCell 

+(NSString *)redirectIdentifier {
返回NSStringFromClass([self class]);
}

-(void)configureWithTitle:(NSString *)title {
self.titleLabel.text =标题;
}

-(IBAction)cellButtonTapped:(id)sender {
BPXLEvent * event = [[BPXLEvent alloc] init];
event.title = self.titleLabel.text;

[[UIApplication sharedApplication]
sendAction:@selector(updateTitle:forEvent :)
至:无
来自:自己
forEvent:event];
}
@结束

有趣的地方发生在cellButtonTapped:中 。 在这里,我们创建一个BPXLEvent,其定义如下:

  @interface BPXLEvent:UIEvent 

@property(copy)NSString * title;

@结束

我们从单元中获取模型信息,并将其提供给要传递的事件。 然后,通过调用-[UIApplication sendAction:to:from:forEvent:]发送以nil为目标的操作。 动作是我们要发送的选择器。 让消费者知道要发送什么动作的一种好方法是创建协议。 为该单元创建的协议在其头文件中定义。 这也可以阻止编译器抱怨未声明的选择器。

  @protocol BPXLTableViewCellActionHandler  

-(void)updateTitle:(id)发送者forEvent:(BPXLEvent *)事件;

@结束

剩下的就是在视图控制器中处理事件。

  @实现BPXLTableViewController 

---剪-

#pragma mark-操作

-(void)updateTitle:(id)发件人forEvent:(BPXLEvent *)事件{
self.title = event.title;
}

@结束

通过实现updateTitle:forEvent:,视图控制器将在每次将操作发送到响应者链时调用它。 真正的强大之处在于,我们不必参与视图控制器中的此事件处理。 如上所述,从广播动作的对象中删除几个级别的视图或视图控制器可以参与。

这到底是怎么回事?

当您 nil用作to:参数调用-[UIApplication sendAction:to:from:forEvent:]时,操作将在应用程序的第一个响应程序处启动。 我设置了一个符号断点(这仅在模拟器中有效,寄存器用于Intel 64位芯片)。

断点触发时的输出为:

  === 
<UITableViewWrapperView:0x7fd062002600; 框架=(0 0; 414736); gestureRecognizers = ; 层= ; contentOffset:{0,0}; contentSize:{414,736}>

(无符号长)$ 1 = 0x00000001041c2562“ updateTitle:forEvent:”
<UITableView:0x7fd062021c00; 框架=(0 0; 414736); clipsToBounds = YES; 自动调整大小= W + H; gestureRecognizers = ; 层= ; contentOffset:{0,-64}; contentSize:{414,308}>

===
<UITableView:0x7fd062021c00; 框架=(0 0; 414736); clipsToBounds = YES; 自动调整大小= W + H; gestureRecognizers = ; 层= ; contentOffset:{0,-64}; contentSize:{414,308}>

(无符号长)$ 4 = 0x00000001041c2562“ updateTitle:forEvent:”


===


(无符号长)$ 7 = 0x00000001041c2562“ updateTitle:forEvent:”
<UIViewControllerWrapperView:0x7fd061c2ab50; 框架=(0 0; 414736); 自动调整大小= W + H; 层= >

在这种情况下,第一响应者是UITableViewWrapperView的实例。 您好,私有API! 发送的动作是updateTitle:forEvent:,其下一个响应者是UITableView的实例。 它不处理该动作,因此它将移至下一个响应者,依此类推。 输出中的最后一个条目是BPXLTableViewController (表视图的视图控制器)。 这是可以处理操作的响应者,因此链条在那里停止。 这是从视图控制器中删除动作处理程序时的样子:

  === 
<UITableViewWrapperView:0x7fbde5828a00; 框架=(0 0; 414736); gestureRecognizers = ; 层= ; contentOffset:{0,0}; contentSize:{414,736}>

(无符号长)$ 1 = 0x000000010923c5ab“ updateTitle:forEvent:”
<UITableView:0x7fbde4024000; 框架=(0 0; 414736); clipsToBounds = YES; 自动调整大小= W + H; gestureRecognizers = ; 层= ; contentOffset:{0,-64}; contentSize:{414,308}>

===
<UITableView:0x7fbde4024000; 框架=(0 0; 414736); clipsToBounds = YES; 自动调整大小= W + H; gestureRecognizers = ; 层= ; contentOffset:{0,-64}; contentSize:{414,308}>

(无符号长)$ 4 = 0x000000010923c5ab“ updateTitle:forEvent:”


===


(无符号长)$ 7 = 0x000000010923c5ab“ updateTitle:forEvent:”
<UIViewControllerWrapperView:0x7fbde5020b20; 框架=(0 0; 414736); 自动调整大小= W + H; 层= >

===
<UIViewControllerWrapperView:0x7fbde5020b20; 框架=(0 0; 414736); 自动调整大小= W + H; 层= >

(无符号长)$ 10 = 0x000000010923c5ab“ updateTitle:forEvent:”
<UINavigationTransitionView:0x7fbde3d214f0; 框架=(0 0; 414736); clipsToBounds = YES; 自动调整大小= W + H; 层= >

===
<UINavigationTransitionView:0x7fbde3d214f0; 框架=(0 0; 414736); clipsToBounds = YES; 自动调整大小= W + H; 层= >

(无符号长)$ 13 = 0x000000010923c5ab“ updateTitle:forEvent:”
<UILayoutContainerView:0x7fbde3c659c0; 框架=(0 0; 414736); 自动调整大小= W + H; gestureRecognizers = ; 层= >

===
<UILayoutContainerView:0x7fbde3c659c0; 框架=(0 0; 414736); 自动调整大小= W + H; gestureRecognizers = ; 层= >

(无符号长)$ 16 = 0x000000010923c5ab“ updateTitle:forEvent:”


===


(无符号长)$ 19 = 0x000000010923c5ab“ updateTitle:forEvent:”
<UIWindow:0x7fbde501cce0; 框架=(0 0; 414736); gestureRecognizers = ; 层= >

===
<UIWindow:0x7fbde501cce0; 框架=(0 0; 414736); gestureRecognizers = ; 层= >

(无符号长)$ 22 = 0x000000010923c5ab“ updateTitle:forEvent:”


===


(无符号长)$ 25 = 0x000000010923c5ab“ updateTitle:forEvent:”


===


(无符号长)$ 28 = 0x000000010923c5ab“ updateTitle:forEvent:”

您会看到它遍历整个响应者链,一直向下到达AppDelegate ,是沿着链的最后一站。 那么,当没有响应者处理该动作时会发生什么呢? -[UIApplication sendAction:to:from:forEvent:]返回BOOL ,该BOOL指示是否已处理该动作。

再一次,这次是Swift

通常,我会在这里停止。 但是我们如何用Swift做到这一点呢? 本质上是相同的技术,但是这次我们在我们的方法中强制执行协议一致性。

对于Objective-C版本,我们从表格视图单元开始。 在这里,我们将从定义协议开始,该协议定义了从组件发送的事件,该组件在响应者链上广播事件。

  @objc协议TitleTableViewCellActionHandler { 
func updateTitleForCell(sender:AnyObject,forEvent event:TitleEvent);
}

与Objective-C版本一样,我们有一个动作,该动作将广播updateTitleForCell(_:forEvent 🙂动作。 这是Swift中的相同代码:

  @IBAction func cellButtonTapped(sender:AnyObject){ 
let event = TitleEvent(标题:titleLabel.text!)

UIApplication.sharedApplication()。sendAction(#selector(TitleTableViewCellActionHandler.updateTitleForCell(_:forEvent :)),到:无,从:自,forEvent:事件)
}

那行得通,但是谁想四处输入UIApplication.sharedApplication()。sendAction(#selector(TitleTableViewCellActionHandler.updateTitleForCell(_:forEvent :))到他们想要的每个动作的nil,from:self,forEvent:event)发送响应者链? 我们可以使它变得容易一些。

首先,我们可以将选择器上移到私有选择器扩展:

 私人分机选择器{ 
静态让TitleUpdated = #selector(TitleTableViewCellActionHandler.updateTitleForCell(_:forEvent :))
}

通过将发送到UIApplication.sharedApplication()。sendAction(.TitleUpdated,发送到:nil,from:self,forEvent:event)的动作转换成一点点。 还有很多,所以让我们创建一个协议和协议扩展。

 协议ResponderChainActionSenderType { 
}

扩展程序ResponderChainActionSenderType {
func sendNilTargetedAction(selector:选择器,
发件人:AnyObject ?,
forEvent事件:UIEvent? = nil)->布尔{
 让application = UIApplication.sharedApplication() 
如果application.targetForAction(selector,
withSender:sender)== nil {
打印(“ \(选择器)未处理”)
}

返回application.sendAction(selector,
至:无,
来自:发件人,
forEvent:事件)
}
}

一旦我们使单元格符合ResponderChainActionSenderType ,我们的动作发送方法就会变成:

  @IBAction func cellButtonTapped(sender:AnyObject){ 
let event = TitleEvent(标题:titleLabel.text!)

sendNilTargetedAction(.TitleUpdated,
发件人:自我,
forEvent:事件)
}

好多了! 剩下要做的就是使我们的视图控制器符合TitleTableViewCellActionHandler并实现适当的方法。

最后说明

响应程序链是在整个应用程序中发送不同类型事件的一种好方法。 它与NSNotificationCenter ,授权和KVO位于同一级别。 这些每个都有自己的用途和误用。 本文概述的用例是我使用响应链在应用程序中分离某些操作的方式之一。 它帮助我将某些用户操作从依赖于视图到视图的控制器关系中分离出来。 这使我可以定义一个动作,并从该动作发送的位置开始进行一些处理。