UIKit: UIControl

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

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

Target-Action机制

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

[buttonaddTarget:selfaction:@selector(tapButton:)forControlEvents:UIControlEventTouchUpInside];


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

有关Target-Action机制的具体描述,大家可以参考Cocoa Application Competencies for iOS – Target Action。我们将会在下面讨论一些Target-action更深入的东西。

实例:一个带Label的图片控件

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

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

提供自定义的跟踪行为。

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


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

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

@interfaceImageControl:UIControl

-(instancetype)initWithFrame:(CGRect)frametitle:(NSString*)titleimage:(UIImage*)image;

@end

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

跟踪触摸事件

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

-(BOOL)beginTrackingWithTouch:(UITouch*)touchwithEvent:(UIEvent*)event

-(BOOL)continueTrackingWithTouch:(UITouch*)touchwithEvent:(UIEvent*)event

-(void)endTrackingWithTouch:(UITouch*)touchwithEvent:(UIEvent*)event

-(void)cancelTrackingWithEvent:(UIEvent*)event

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

-(void)touchesBegan:(NSSet<UITouch*>*)toucheswithEvent:(UIEvent*)event

-(void)touchesMoved:(NSSet<UITouch*>*)toucheswithEvent:(UIEvent*)event

-(void)touchesEnded:(NSSet<UITouch*>*)toucheswithEvent:(UIEvent*)event

-(void)touchesCancelled:(NSSet<UITouch*>*)toucheswithEvent:(UIEvent*)event

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

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

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

观察或修改分发到target对象的行为消息

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

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

// ImageControl.m

-(void)sendAction:(SEL)actionto:(id)targetforEvent:(UIEvent*)event{

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

[supersendAction:@selector(handleAction:)to:selfforEvent:event];

}

-(void)handleAction:(id)sender{

NSLog(@"handle Action");

}

// ViewController.m

-(void)viewDidLoad{

[superviewDidLoad];

self.view.backgroundColor=[UIColorwhiteColor];

ImageControl*control=[[ImageControlalloc]initWithFrame:(CGRect){50.0f,100.0f,200.0f,300.0f}title:@"This is a demo"image:[UIImageimageNamed:@"demo"]];

// ...

[controladdTarget:selfaction:@selector(tapImageControl:)forControlEvents:UIControlEventTouchUpInside];

}

-(void)tapImageControl:(id)sender{

NSLog(@"sender = %@",sender);

}

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

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

-(void)viewDidLoad{

// ...

[controladdTarget:selfaction:@selector(tapImageControl:)forControlEvents:UIControlEventTouchUpInside];

[controlsendActionsForControlEvents:UIControlEventTouchUpInside];

}

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

Target-Action的管理

// 添加

-(void)addTarget:(id)targetaction:(SEL)actionforControlEvents:(UIControlEvents)controlEvents

-(void)removeTarget:(id)targetaction:(SEL)actionforControlEvents:(UIControlEvents)controlEvents

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

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

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

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

因此,UIControl内部实际上是有一个可变数组(_targetActions)来保存Target-Action,数组中的每个元素是一个UIControlTargetAction对象。UIControlTargetAction类是一个私有类,我们可以在iOS-Runtime-Header中找到它的头文件:

@interfaceUIControlTargetAction:NSObject{

SEL_action;

BOOL_cancelled;

unsignedint_eventMask;

id_target;

}

@property(nonatomic)BOOLcancelled;

-(void).cxx_destruct;

-(BOOL)cancelled;

-(void)setCancelled:(BOOL)arg1;

@end

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

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


嗯,循环引用。

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

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


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


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

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

When you call this method, target is not retained.

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

小结

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

示例工程的代码已上传到github,可以在这里下载。另外,推荐一下SVSegmentedControl这个控件,大家可以研究下它的实现。

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

推荐阅读更多精彩内容