首页 > 其他分享 >「OC」剪不断,理还乱——UIResponder、UIGestureRecognizer、UIControl的响应优先级探究

「OC」剪不断,理还乱——UIResponder、UIGestureRecognizer、UIControl的响应优先级探究

时间:2024-09-04 21:52:13浏览次数:11  
标签:识别器 UIControl self UIResponder OC 事件 手势 view

「OC」剪不断,理还乱——UIResponder、UIGestureRecognizer、UIControl的响应优先级探究

文章目录

前言

上一篇我们记录了编译器是如何找到最佳响应者以及响应者链的构成「OC」初识iOS事件处理流程,那么今天我们就来探究,UIResponder、UIGestureRecognizer、UIControl在处理事件时的优先级,以及相关的细节。让我们开始UIResponder、UIGestureRecognizer、UIControl的爱恨情仇吧

介绍

先简单介绍一下本文的三个主角

UIResponder

UIResponder是iOS中处理事件的核心类,它是许多重要UI类的父类,包括UIApplication、UIView和UIViewController。

主要特点:

  • 定义了响应和处理事件的接口
  • 是构建响应者链的基础
  • 可以处理触摸事件、运动事件、远程控制事件等

UIGestureRecognizer

UIGestureRecognizer是一个抽象基类,用于检测特定的触摸手势。它的继承了UIView,而UIView又继承于UIResponder,所以在某些时候我们可以将两者等同看待,在细节方面有相似之处

主要特点:

  • 简化了复杂手势的识别过程
  • 可以添加到任何UIView上
  • 有多个具体子类,如UIPanGestureRecognizer, UITapGestureRecognizer等

补充:其实手势接受事件也是通过与其他两者类似的方式,即触摸开始——触摸移动——触摸取消——触摸结束,他们被写在了UIGestureRecognizerSubclass.h之中,我们下文探究触摸优先级时,打印日志需要在UITapGestureRecognizer的子类之中引入 import <UIKit/UIGestureRecognizerSubclass.h>

手势分为离散型手势(discrete gestures)和持续型手势(continuous gesture)。系统提供的离散型手势包括点按手势(UITapGestureRecognizer)和轻扫手势(UISwipeGestureRecognizer),其余均为持续型手势。

在手势之中,有三个属性比较重要

@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;

cancelsTouchesInView:默认值为YES,表示当手势识别器成功识别了手势之后,会通知Application取消响应链对事件的响应,并不再传递事件给hit-test view。若设置成NO,表示手势识别成功后不取消响应链对事件的响应,事件依旧会传递给hit-test view。

delaysTouchesBegan:默认为NO。默认情况下手势识别器在识别手势期间,当触摸状态发生改变时,Application都会将事件传递给手势识别器和hit-tested view;若设置成YES,则表示手势识别器在识别手势期间,截断事件,即不会将事件发送给hit-tested view。

delaysTouchesEnded:默认为YES。当手势识别失败时,若此时触摸已经结束,会延迟一小段时间(0.15s)再调用响应者的 touchesEnded:withEvent:;若设置成NO,则在手势识别失败时会立即通知Application发送状态为end的touch事件给hit-tested view以调用 touchesEnded:withEvent: 结束事件响应。

UIControl

UIControl是一个抽象基类,为特定的用户界面元素(如按钮、开关、滑块等)提供了基础结构。

主要特点:

  • 继承自UIView
  • 定义了一组标准的控制事件(如触摸按下、抬起、拖动等)
  • 允许为不同事件添加多个目标动作

target-action

  • target:处理交互事件的对象
  • action:处理交互事件的方式

UIControl作为能够响应事件的控件,必然也需要待事件交互符合条件时才去响应,因此也会跟踪事件发生的过程。不同于UIResponder以及UIGestureRecognizer通过 touches 系列方法跟踪,UIControl有其独特的跟踪方法:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;

大致方法其实与我们先前接触的UIResponder的那4个方法相同,毕竟UIControl本身也是UIResponder的子类,但是这个UIControl之中的tracking系列的方法是在touch系列之中进行实现的。

具体的target-action具体流程,我按着网上给出的内容叙述一遍:

UIControl控件通过 addTarget:action:forControlEvents: 添加事件处理的target和action,当事件发生时,会调用 sendAction:to:forEvent: 将target、action以及event对象发送给全局应用,Application对象再通过 sendAction:to:from:forEvent: 向target发送action。

img

另外,若不指定target,即 addTarget:action:forControlEvents: 时target传空,那么当事件发生时,Application会在响应链上从上往下寻找能响应action的对象。

正文

接下来我们来探究一下,事件产生的先后优先顺序

我们先写一个简单的demo进行测试

#import "ViewController.h"

@interface ViewController () <UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray *dataSource;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"手势冲突测试";
    self.view.backgroundColor = [UIColor whiteColor];
    
    // 初始化数据源
    self.dataSource = @[@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8"];
    
    // 创建并设置UITableView
    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    self.tableView.backgroundColor = [UIColor orangeColor];
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
    [self.view addSubview:self.tableView];
    
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(actionTapView)];
        [self.view addGestureRecognizer:tap];

    UIButton *bottomButton = [UIButton buttonWithType:UIButtonTypeCustom];
    [bottomButton setTitle:@"点我无压力" forState:UIControlStateNormal];
    [bottomButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    bottomButton.backgroundColor = [UIColor cyanColor];
    bottomButton.frame = CGRectMake(20, CGRectGetMaxY(self.view.bounds) - 60, CGRectGetWidth(self.view.bounds) - 40, 40);
    [bottomButton addTarget:self action:@selector(buttonTap) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:bottomButton];
}

-(void)buttonTap {
    NSLog(@"button clicked!");
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataSource.count;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 55;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
        cell.backgroundColor = [UIColor orangeColor];
        cell.textLabel.textColor = [UIColor whiteColor];
    }
    
    cell.textLabel.text = self.dataSource[indexPath.row];
    
    return cell;
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"cell selected!");
}

- (void)actionTapView{
    NSLog(@"backview taped");
}

@end

我们进行点击进行尝试

Sep-03-2024 21-21-52

可以发现当我短按cell的时候,触发的是手势事件,只有长按cell才能够触发点击cell的方法,这到底是为什么啊?

由于这三兄弟,都实现了类似的方法,所以我在他们的子类之中,增加的打印日志的行为,以探究这三者的运行关系

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

具体打印日志的方式如下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    [super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    [super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    [super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    [super touchesCancelled:touches withEvent:event];
}

UIGestureRecognizer和UIResponder

关于UIGestureRecognizer和UIResponder的问题,其实上面UIGestureRecognizer的属性已经给我们透露一些相关的内容了,那我们就先从我们最有疑问的开始探究吧。

短按

image-20240904155530051

在短按的时候我们可以看到,其实在我们短按的时候,手势识别先开始,而tableView的触摸在随后也开始了,之所以我们的无法触发选择cell的方法是因为,cell的触摸在手势触发之后被Application给取消了。

引用官方文档的翻译:Window在将事件传递给hit-tested view之前,会先将事件传递给相关的手势识别器并由手势识别器优先识别。若手势识别器成功识别了事件,就会取消hit-tested view对事件的响应;若手势识别器没能识别事件,hit-tested view才完全接手事件的响应权。

因此通过这个点,我们可以总结出:手势识别器比UIResponder具有更高的事件响应优先级!!

快速按下

有的读者可能就有疑惑了,咋一听,如果短按的道理是上面解释的那样,那么按理来说快速按下的响应过程和短按应该是相同的才对。

我们知道UITableView是UIScrollView的子类,而UIScrollView因为要滚动,所以对事件做了特殊的处理: 当UIScrollView接收到事件之后,会暂时劫持当前的事件,会产生一个UIScrollViewDelayedTouchesBeganGestureRecognizer的手势识别器识别是否进行滚动操作,由于手势操作在应用之中可以进行同时识别,所以当UIScrollView这个机制劫持了触摸事件,和TableViewtouchesBegan: 的调用时间相差大概为0.15s,由于UIScrollViewDelayedTouchesBeganGestureRecognizer拦截了touchesBegan: 发送,当我们点击时间过短的话,没过延迟时间触摸就结束了,因此就会出现以下情况。

请添加图片描述

长按

由于我们手势劫持判断是有时效的,当时效结束之后,hit-tested view才完全接手事件,停止向手势识别器发送事件,仅向hit-test view发送事件。

image-20240904155620433

补充

刚刚我们讲的是离散型的手势,接下来我们补充一下持续性手势,仍是相同的内容但是把手势改为持续性的手势,

image-20240904200533662

我们可以看到即便滑动手势识别器识别了手势,Application也会依旧发送事件给tableView。

另外,在滑动的过程中,若手势识别器未能识别手势,则事件在触摸滑动过程中会一直传递给hit-tested view,直到触摸结束。

乱上加乱

如果让本就纠缠不清,复杂无比的UIGestureRecognizer和UIResponder,再有第三者UIControl插足呢?

真是头大,看的我都快看不懂字了。

根据官方文档,总结内容如下:UIControl会阻止父视图上的手势识别器行为,也就是UIControl处理事件的优先级比UIGestureRecognizer高,但前提是相比于父视图上的手势识别器。

我们设置一下情景

请添加图片描述

在view上添加点击手势识别器

image-20240904210011938

点击button后,事件先传递给手势识别器,再传递给作为hit-tested view存在的button(UIControl本身也是UIResponder),由于button阻止了父视图BlueView中的手势识别器的识别,导致手势识别器识别失败,button完全接手了事件的响应权,事件最终由button响应;

在button上添加手势识别器

请添加图片描述

点击button后,事件先传递给手势识别器,再传递给作为hit-tested view存在的button(UIControl本身也是UIResponder),button未阻止其本身绑定的手势识别器的识别,因此手势识别器先识别手势并识别成功,而后通知Application取消响应链对事件的响应,因为 touchesCancelled 被调用,同时 cancelTrackingWithEvent 跟着调用,因此button的target-action得不到执行。

所以我们可以得出结论:

UIControl比其父视图上的手势识别器具有更高的事件响应优先级。

总结

最后我们可以笼统的得出一个结论:UIRespnder、UIGestureRecognizer、UIControl,笼统地讲,事件响应优先级依次递增。

参考资料

01 触摸事件传递

iOS事件处理

iOS——事件、响应链和传递链

iOS触摸事件全家桶

UIGestureRecognizer

标签:识别器,UIControl,self,UIResponder,OC,事件,手势,view
From: https://blog.csdn.net/aa2002aa/article/details/141904718

相关文章

  • 【python】socket 入门以及多线程tcp链接
    Socket入门及多线程tcp链接网络基础知识三要素Socket是套接字的意思,是网络编程的核心对象,通信两端都独有自己的Socket对象,数据在两个Socket之间通过字节流(TCP协议)或者数据报包(UDP协议)的形式进行传输.本文主要针对tcp流程进行讲解socket-tcp流程图1.创建......
  • 【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)
    【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?Docker架构Docker采用client/server架构,客户端向服务器发送请求,服务器负责构建、运行和分发容器:Docker架构说明:我们日常使用各种docker命令,如dockerrun、dockerpull等,其实就是在使用Docker客户端(DockerCLI);客户端将用户输......
  • rocky linux 9部署zabbix6
    安装rocky9阿里巴巴开源镜像站http://mirrors.aliyun.com1、Rocky2、初始化防火墙        systemctlstopfirewalld        systemctldisablefilewalld        或者        systemctldisablefirewalld--now3、selinuxvi/etc......
  • java 使用WebSocket 功能实现
    java使用WebSocket功能实现我整理的一些关于【架构设计】的项目学习资料+视频(附讲解~~)和大家一起分享、学习一下: https://d.51cto.com/bLN8S1实现Java直播弹幕功能1.引言本文将教会一位刚入行的小白如何实现Java直播弹幕功能。直播弹幕是指在直播过程中,观众可以发送实时弹幕......
  • 在本地通过Docker安装RocketMQ
    拉取镜像&部署这里选用foxiswho/rocketmq:server-4.5.1版本,在官方镜像没出来前,foxiswho是一个比较靠谱的第三方镜像。执行下面的命令直接启动NameServer。dockerrun-d-p9876:9876--namermqnamesrvfoxiswho/rocketmq:server-4.5.1接下来执行下面的命令启动Broker,......
  • Java 入门指南:Java 并发编程 —— 并发容器 PriorityBlockingQueue
    BlockingQueueBlockingQueue是Java并发包(java.util.concurrent)中提供的一个阻塞队列接口,它继承自Queue接口。BlockingQueue中的元素采用FIFO的原则,支持多线程环境并发访问,提供了阻塞读取和写入的操作,当前线程在队列满或空的情况下会被阻塞,直到被唤醒或超时。常用的......
  • 如何使用Docker Compose运行Nexus并将本地仓库管理工具发布至公网
    文章目录前言1.Docker安装Nexus2.本地访问Nexus3.Linux安装Cpolar4.配置Nexus界面公网地址5.远程访问Nexus界面6.固定Nexus公网地址7.固定地址访问Nexus前言本文主要介绍在Linux中使用Docker来一键部署NexusMaven私有仓库工具并结合Cpolar内网穿透实现远程......
  • C# WebSocket高并发通信阻塞问题
    项目上遇到使用WebSocket超时问题,具体情况是这样的,OTA升级过程中,解压zip文件会有解压进度事件,将解压进度通过进程通信传给另一进程,通信提示超时异常小伙伴堂园发现大文件使用Zip解压,解压进度事件间隔竟然是1ms,简直超大频率啊但是,解压事件超频也不应该通信异常啊,于是我通过1ms定......
  • Centos7.9安装Docker和Docker compose
    什么是docker环境Docker环境是指在计算机中安装和配置了Docker引擎的运行环境。Docker是一种容器化平台,它提供了一种轻量级的虚拟化技术,能够将应用程序及其依赖项打包成一个独立的容器,以实现快速部署、可移植性和易于管理的优势。(Docker环境提供了一种方便、可移植和隔离的方式来......
  • win11+docker desktop导入镜像运行容器
    一:配置环境前置条件:Win11环境,docker注册账号1.安装WSL(1)在windowsstore安装Ubuntu20.04.6(2)设置“控制面板”-“程序”-“程序和功能”-左侧“启用或关闭Windows功能”-打开“适用于Linux的Windows子系统”,“Windows虚拟机监控程序平台”(3)终端执行:sudoaptupdatesudoapt......