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

2023-12-06 02:08

本文主要是介绍UIControl 的基本使用方法和 Target-Action 机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

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

Target-Action机制

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

1
[button addTarget:self action:@selector(tapButton:) forControlEvents:UIControlEventTouchUpInside];
也就是说,当按钮的点击事件发生时,会将消息发送到target(此处即为self对象),并由target对象的tapButton:方法来处理相应的事件。其基本过程可以用下图来描述:

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

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

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

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

对于特定的事件,我们需要观察或修改分发到target对象的行为消息。
提供自定义的跟踪行为。
本例将会简单地结合这两者。先来看看效果:
这个控件很简单,以图片为背景,然后在下方显示一个Label。

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

@interface ImageControl : UIControl

  • (instancetype)initWithFrame:(CGRect)frame title:(NSString )title image:(UIImage )image;

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

跟踪触摸事件
如果是想提供自定义的跟踪行为,则可以重写以下几个方法:
- (BOOL)beginTrackingWithTouch:(UITouch )touch withEvent:(UIEvent )event
- (BOOL)continueTrackingWithTouch:(UITouch )touch withEvent:(UIEvent )event
- (void)endTrackingWithTouch:(UITouch )touch withEvent:(UIEvent )event
- (void)cancelTrackingWithEvent:(UIEvent *)event
这四个方法分别对应的时跟踪开始、移动、结束、取消四种状态。看起来是不是很熟悉?这跟UIResponse提供的四个事件跟踪方法是不是挺像的?我们来看看UIResponse的四个方法:
- (void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event
- (void)touchesMoved:(NSSet )touches withEvent:(UIEvent )event
- (void)touchesEnded:(NSSet )touches withEvent:(UIEvent )event
- (void)touchesCancelled:(NSSet )touches withEvent:(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)action to:(id)target forEvent:(UIEvent *)event {
// 将事件传递到对象本身来处理
[super sendAction:@selector(handleAction:) to:self forEvent:event];
}

  • (void)handleAction:(id)sender {

    NSLog(@”handle Action”);
    }

// ViewController.m

  • (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];

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

    [control addTarget:self action:@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 {
// …
[control addTarget:self action:@selector(tapImageControl:) forControlEvents:UIControlEventTouchUpInside];

[control sendActionsForControlEvents:UIControlEventTouchUpInside];

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

为一个控件对象添加、删除Target-Action的操作我们都已经很熟悉了,主要使用的是以下两个方法:
// 添加
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents

  • (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
    如果想获取控件对象所有相关的target对象,则可以调用allTargets方法,该方法返回一个集合。集合中可能包含NSNull对象,表示至少有一个nil目标对象。

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

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

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

因此,UIControl内部实际上是有一个可变数组(_targetActions)来保存Target-Action,数组中的每个元素是一个UIControlTargetAction对象。UIControlTargetAction类是一个私有类,我们可以在iOS-Runtime-Header中找到它的头文件
@interface UIControlTargetAction : NSObject {
SEL _action;
BOOL _cancelled;
unsigned int _eventMask;
id _target;
}

@property (nonatomic) BOOL cancelled;

  • (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断点,如下所示:

blob.png

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

blob.png

可以看到,对于_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这个控件,大家可以研究下它的实现。

这篇关于UIControl 的基本使用方法和 Target-Action 机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/460001

相关文章

Python 字典 (Dictionary)使用详解

《Python字典(Dictionary)使用详解》字典是python中最重要,最常用的数据结构之一,它提供了高效的键值对存储和查找能力,:本文主要介绍Python字典(Dictionary)... 目录字典1.基本特性2.创建字典3.访问元素4.修改字典5.删除元素6.字典遍历7.字典的高级特性默认字典

使用Python构建一个高效的日志处理系统

《使用Python构建一个高效的日志处理系统》这篇文章主要为大家详细讲解了如何使用Python开发一个专业的日志分析工具,能够自动化处理、分析和可视化各类日志文件,大幅提升运维效率,需要的可以了解下... 目录环境准备工具功能概述完整代码实现代码深度解析1. 类设计与初始化2. 日志解析核心逻辑3. 文件处

Nginx安全防护的多种方法

《Nginx安全防护的多种方法》在生产环境中,需要隐藏Nginx的版本号,以避免泄漏Nginx的版本,使攻击者不能针对特定版本进行攻击,下面就来介绍一下Nginx安全防护的方法,感兴趣的可以了解一下... 目录核心安全配置1.编译安装 Nginx2.隐藏版本号3.限制危险请求方法4.请求限制(CC攻击防御)

python生成随机唯一id的几种实现方法

《python生成随机唯一id的几种实现方法》在Python中生成随机唯一ID有多种方法,根据不同的需求场景可以选择最适合的方案,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习... 目录方法 1:使用 UUID 模块(推荐)方法 2:使用 Secrets 模块(安全敏感场景)方法

解决1093 - You can‘t specify target table报错问题及原因分析

《解决1093-Youcan‘tspecifytargettable报错问题及原因分析》MySQL1093错误因UPDATE/DELETE语句的FROM子句直接引用目标表或嵌套子查询导致,... 目录报js错原因分析具体原因解决办法方法一:使用临时表方法二:使用JOIN方法三:使用EXISTS示例总结报错原

一文详解如何使用Java获取PDF页面信息

《一文详解如何使用Java获取PDF页面信息》了解PDF页面属性是我们在处理文档、内容提取、打印设置或页面重组等任务时不可或缺的一环,下面我们就来看看如何使用Java语言获取这些信息吧... 目录引言一、安装和引入PDF处理库引入依赖二、获取 PDF 页数三、获取页面尺寸(宽高)四、获取页面旋转角度五、判断

MyBatis-Plus通用中等、大量数据分批查询和处理方法

《MyBatis-Plus通用中等、大量数据分批查询和处理方法》文章介绍MyBatis-Plus分页查询处理,通过函数式接口与Lambda表达式实现通用逻辑,方法抽象但功能强大,建议扩展分批处理及流式... 目录函数式接口获取分页数据接口数据处理接口通用逻辑工具类使用方法简单查询自定义查询方法总结函数式接口

C++中assign函数的使用

《C++中assign函数的使用》在C++标准模板库中,std::list等容器都提供了assign成员函数,它比操作符更灵活,支持多种初始化方式,下面就来介绍一下assign的用法,具有一定的参考价... 目录​1.assign的基本功能​​语法​2. 具体用法示例​​​(1) 填充n个相同值​​(2)

MySql基本查询之表的增删查改+聚合函数案例详解

《MySql基本查询之表的增删查改+聚合函数案例详解》本文详解SQL的CURD操作INSERT用于数据插入(单行/多行及冲突处理),SELECT实现数据检索(列选择、条件过滤、排序分页),UPDATE... 目录一、Create1.1 单行数据 + 全列插入1.2 多行数据 + 指定列插入1.3 插入否则更

MySQL深分页进行性能优化的常见方法

《MySQL深分页进行性能优化的常见方法》在Web应用中,分页查询是数据库操作中的常见需求,然而,在面对大型数据集时,深分页(deeppagination)却成为了性能优化的一个挑战,在本文中,我们将... 目录引言:深分页,真的只是“翻页慢”那么简单吗?一、背景介绍二、深分页的性能问题三、业务场景分析四、