天天看点

UIControl 的基本使用方法和 Target-Action 机制

我们在开发应用的时候,经常会用到各种各样的控件,诸如按钮(<code>UIButton</code>)、滑块(<code>UISlider</code>)、分页控件(<code>UIPageControl</code>)等。这些控件用来与用户进行交互,响应用户的操作。我们查看这些类的继承体系,可以看到它们都是继承于<code>UIControl</code>类。<code>UIControl</code>是控件类的基类,它是一个抽象基类,我们不能直接使用<code>UIControl</code>类来实例化控件,它只是为控件子类定义一些通用的接口,并提供一些基础实现,以在事件发生时,预处理这些消息并将它们发送到指定目标对象上。

本文将通过一个自定义的<code>UIControl</code>子类来看看<code>UIControl</code>的基本使用方法。不过在开始之前,让我们先来了解一下<code>Target-Action</code>机制。

<code>Target-action</code>是一种设计模式,直译过来就是”目标-行为”。当我们通过代码为一个按钮添加一个点击事件时,通常是如下处理:

1

<code>[button addTarget:self action:@selector(tapButton:) forControlEvents:UIControlEventTouchUpInside];</code>

也就是说,当按钮的点击事件发生时,会将消息发送到<code>target</code>(此处即为self对象),并由<code>target</code>对象的<code>tapButton:</code>方法来处理相应的事件。其基本过程可以用下图来描述:

UIControl 的基本使用方法和 Target-Action 机制

即当事件发生时,事件会被发送到控件对象中,然后再由这个控件对象去触发<code>target</code>对象上的<code>action</code>行为,来最终处理事件。因此,<code>Target-Action</code>机制由两部分组成:即目标对象和行为<code>Selector</code>。目标对象指定最终处理事件的对象,而行为<code>Selector</code>则是处理事件的方法。

回到我们的正题来,我们将实现一个带Label的图片控件。通常情况下,我们会基于以下两个原因来实现一个自定义的控件:

对于特定的事件,我们需要观察或修改分发到<code>target</code>对象的行为消息。

提供自定义的跟踪行为。

本例将会简单地结合这两者。先来看看效果:

UIControl 的基本使用方法和 Target-Action 机制

这个控件很简单,以图片为背景,然后在下方显示一个Label。

先创建<code>UIControl</code>的一个子类,我们需要传入一个字符串和一个UIImage对象:

2

3

4

5

<code>@interface ImageControl : UIControl</code>

<code>- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title image:(UIImage *)image;</code>

<code>@end</code>

基础的布局我们在此不讨论。我们先来看看<code>UIControl</code>为我们提供了哪些自定义跟踪行为的方法。

如果是想提供自定义的跟踪行为,则可以重写以下几个方法:

<code>- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event</code>

<code>- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event</code>

<code>- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event</code>

<code>- (void)cancelTrackingWithEvent:(UIEvent *)event</code>

这四个方法分别对应的时跟踪开始、移动、结束、取消四种状态。看起来是不是很熟悉?这跟<code>UIResponse</code>提供的四个事件跟踪方法是不是挺像的?我们来看看<code>UIResponse</code>的四个方法:

<code>- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event</code>

<code>- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event</code>

<code>- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event</code>

<code>- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event</code>

我们可以看到,上面两组方法的参数基本相同,只不过<code>UIControl</code>的是针对单点触摸,而<code>UIResponse</code>可能是多点触摸。另外,返回值也是大同小异。由于<code>UIControl</code>本身是视图,所以它实际上也继承了<code>UIResponse</code>的这四个方法。如果测试一下,我们会发现在针对控件的触摸事件发生时,这两组方法都会被调用,而且互不干涉。

为了判断当前对象是否正在追踪触摸操作,<code>UIControl</code>定义了一个<code>tracking</code>属性。该值如果为YES,则表明正在追踪。这对于我们是更加方便了,不需要自己再去额外定义一个变量来做处理。

在测试中,我们可以发现当我们的触摸点沿着屏幕移出控件区域名,还是会继续追踪触摸操作,<code>cancelTrackingWithEvent:</code>消息并未被发送。为了判断当前触摸点是否在控件区域类,可以使用<code>touchInside</code>属性,这是个只读属性。不过实测的结果是,在控件区域周边一定范围内,该值还是会被标记为YES,即用于判定<code>touchInside</code>为YES的区域会比控件区域要大。

对于一个给定的事件,<code>UIControl</code>会调用<code>sendAction:to:forEvent:</code>来将行为消息转发到<code>UIApplication</code>对象,再由<code>UIApplication</code>对象调用其<code>sendAction:to:fromSender:forEvent:</code>方法来将消息分发到指定的<code>target</code>上,而如果我们没有指定<code>target</code>,则会将事件分发到响应链上第一个想处理消息的对象上。而如果子类想监控或修改这种行为的话,则可以重写这个方法。

在我们的实例中,做了个小小的处理,将外部添加的<code>Target-Action</code>放在控件内部来处理事件,因此,我们的代码实现如下:

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

<code>// ImageControl.m</code>

<code>- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {</code>

<code>  </code><code>// 将事件传递到对象本身来处理</code>

<code>    </code><code>[</code><code>super</code> <code>sendAction:@selector(handleAction:) to:self forEvent:event];</code>

<code>}</code>

<code>- (void)handleAction:(id)sender {</code>

<code>    </code><code>NSLog(@</code><code>"handle Action"</code><code>);</code>

<code>// ViewController.m</code>

<code>- (void)viewDidLoad {</code>

<code>    </code><code>[</code><code>super</code> <code>viewDidLoad];</code>

<code>    </code><code>self.view.backgroundColor = [UIColor whiteColor];</code>

<code>    </code><code>ImageControl *control = [[ImageControl alloc] initWithFrame:(CGRect){50.0f, 100.0f, 200.0f, 300.0f} title:@</code><code>"This is a demo"</code> <code>image:[UIImage imageNamed:@</code><code>"demo"</code><code>]];</code>

<code>    </code><code>// ...</code>

<code>    </code><code>[control addTarget:self action:@selector(tapImageControl:) forControlEvents:UIControlEventTouchUpInside];</code>

<code>- (void)tapImageControl:(id)sender {</code>

<code>    </code><code>NSLog(@</code><code>"sender = %@"</code><code>, sender);</code>

由于我们重写了<code>sendAction:to:forEvent:</code>方法,所以最后处理事件的<code>Selector</code>是<code>ImageControl</code>的<code>handleAction:</code>方法,而不是ViewController的<code>tapImageControl:</code>方法。

另外,<code>sendAction:to:forEvent:</code>实际上也被<code>UIControl</code>的另一个方法所调用,即<code>sendActionsForControlEvents:</code>。这个方法的作用是发送与指定类型相关的所有行为消息。我们可以在任意位置(包括控件内部和外部)调用控件的这个方法来发送参数<code>controlEvents</code>指定的消息。在我们的示例中,在ViewController.m中作了如下测试:

<code>    </code><code>[control sendActionsForControlEvents:UIControlEventTouchUpInside];</code>

可以看到在未点击控件的情况下,触发了<code>UIControlEventTouchUpInside</code>事件,并打印了<code>handle Action</code>日志。

为一个控件对象添加、删除<code>Target-Action</code>的操作我们都已经很熟悉了,主要使用的是以下两个方法:

<code>// 添加</code>

<code>- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents</code>

<code>- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents</code>

如果想获取控件对象所有相关的target对象,则可以调用<code>allTargets</code>方法,该方法返回一个集合。集合中可能包含<code>NSNull</code>对象,表示至少有一个nil目标对象。

而如果想获取某个target对象及事件相关的所有action,则可以调用<code>actionsForTarget:forControlEvent:</code>方法。

不过,这些都是<code>UIControl</code>开放出来的接口。我们还是想要探究一下,<code>UIControl</code>是如何去管理<code>Target-Action</code>的呢?

实际上,我们在程序某个合适的位置打个断点来观察<code>UIControl</code>的内部结构,可以看到这样的结果:

UIControl 的基本使用方法和 Target-Action 机制

<code>@interface UIControlTargetAction : NSObject {</code>

<code>    </code><code>SEL _action;</code>

<code>    </code><code>BOOL _cancelled;</code>

<code>    </code><code>unsigned int _eventMask;</code>

<code>    </code><code>id _target;</code>

<code>@property (nonatomic) BOOL cancelled;</code>

<code>- (void).cxx_destruct;</code>

<code>- (BOOL)cancelled;</code>

<code>- (void)setCancelled:(BOOL)arg1;</code>

可以看到<code>UIControlTargetAction</code>对象维护了一个<code>Target-Action</code>所必须的三要素,即<code>target</code>,<code>action</code>及对应的事件<code>eventMask</code>。

如果仔细想想,会发现一个有意思的问题。我们来看看实例中ViewController(target)与ImageControl实例(control)的引用关系,如下图所示:

UIControl 的基本使用方法和 Target-Action 机制

嗯,循环引用。

既然这样,就必须想办法打破这种循环引用。那么在这5个环节中,哪个地方最适合做这件事呢?仔细思考一样,1、2、4肯定是不行的,3也不太合适,那就只有5了。在上面的<code>UIControlTargetAction</code>头文件中,并没有办法看出<code>_target</code>是以<code>weak</code>方式声明的,那有证据么?

我们在工程中打个<code>Symbolic</code>断点,如下所示:

UIControl 的基本使用方法和 Target-Action 机制

运行程序,程序会进入<code>[UIControl addTarget:action:forControlEvents:]</code>方法的汇编代码页,在这里,我们可以找到一些蛛丝马迹。如下图所示:

UIControl 的基本使用方法和 Target-Action 机制

可以看到,对于<code>_target</code>成员变量,在<code>UIControlTargetAction</code>的初始化方法中调用了<code>objc_storeWeak</code>,即这个成员变量对外部传进来的<code>target</code>对象是以<code>weak</code>的方式引用的。

其实在<code>UIControl</code>的文档中,<code>addTarget:action:forControlEvents:</code>方法的说明还有这么一句:

When you call this method, target is not retained.

另外,如果我们以同一组target-action和event多次调用<code>addTarget:action:forControlEvents:</code>方法,在<code>_targetActions</code>中并不会重复添加<code>UIControlTargetAction</code>对象。

控件是我们在开发中常用的视图工具,能很好的表达用户的意图。我们可以使用UIKit提供的控件,也可以自定义控件。当然,<code>UIControl</code>除了上述的一些方法,还有一些属性和方法,以及一些常量,大家可以参考文档。

<a target="_blank" href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIControl_Class">UIControl Class Reference</a>

<a target="_blank" href="https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/UIControl.html#//apple_ref/doc/uid/TP40012857-UIControl">UIKit User Interface Catalog – About Controls</a>

<a target="_blank" href="https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/Devpedia-CocoaApp/TargetAction.html">Cocoa Application Competencies for iOS – Target Action</a>

<a target="_blank" href="https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/UIKit.framework/UIControlTargetAction.h">iOS-Runtime-Header: UIControlTargetAction</a>

<a target="_blank" href="https://github.com/samvermette/SVSegmentedControl">SVSegmentedControl</a>

继续阅读