遍历当前app的所有windows,寻找到等级为UIWindowLevelNormal的window
拿到该window的根控制器,循环遍历这个根控制器的presentedViewController直到找到当前显示的viewcontroller
参考的代码如下:
NSArray windows = [UIApplication sharedApplication]windows;
UIWindow keyWindow;
for (UIWindow window in windows) {
if (windowwindowLevel == UIWindowLevelNormal) {
keyWindow = window;
break;
}
}
UIViewController topRootController = keyWindowrootViewController;
while (topRootControllerpresentedViewController) {
topRootController = topRootControllerpresentedViewController;
}
最后的topRootController即为当前显示的视图控制器
下面是比较严谨的获取方法:
- (UIWindow )lastWindow
{
NSArray windows = [UIApplication sharedApplication]windows;
for(UIWindow window in [windows reverseObjectEnumerator]) {
if ([window isKindOfClass:[UIWindow class]] &&
CGRectEqualToRect(windowbounds, [UIScreen mainScreen]bounds))
return window;
}
return [UIApplication sharedApplication]keyWindow;
}
1、系统响应阶段:触摸屏幕→IOKit(IOHIDEvent)→通过mach port(IPC进程间通信)转发给SpringBoard(IOHIDEvent)→SpringBoard通过当前桌面状态判断前台运行的app,并通过mach port转发给当前app
2、应用内部:当前app主线程RunLoop的Source1(监听mach port消息,接收SpringBoard事件)触发,并在Source0回调内部把IOHIDEvent封装为 UIEvent, 调用UIApplication的sendEvent把UIEvent传给UIWindow,开始通过hitTest:view寻找最佳响应者( UIResponder ),找到最佳响应者后,事件开始在响应链中传递,最终被某个响应者/手势识别器/Target-Action模式捕捉并消耗掉,或没有找到任何响应对象后释放。
注:Mach Port进程端口,各进程利用它进行通信。
注:SpringBoard是一个系统进程,即桌面系统,统一管理和分发触摸事件。
事件的传递和分发,其实是寻找最佳响应者的过程(Hit-Testing 命中检测)。事件的传递起源于触摸状态的变化,一个点击会触发两次事件的传递(begin到end,touch的状态发生变化)
核心方法
1、- (UIView )hitTest:(CGPoint)point withEvent:(UIEvent )event {} // 视图是否能够响应事件
2、- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent )event {} // 触摸点是否在hitTest方法返回的视图内
传递过程 :UIApplication
→调用UIWindow的hitTest(多个window询问后显示的window(所以新添加的window记得makeKeyAndVisible))
→递归询问子视图是否能响应事件(实现:判断3种无法响应的情况→判断触摸点(return nil),从后往前遍历子视图(i = count - 1),convertPoint坐标转换到子视图上,并递归调用hitTest询问子视图中的子视图,如果有合适的则return,没有则return self)
→Window的hitTest方法返回最佳响应者,告知UIApplication。
注:无法响应的3种情况userInteractionEnabled = NO;hidden = YES;alpha < 001
事件拦截 :定制视图,自定义事件流向。如遇到超出TabBar坐标范围的TabButton时,重写TabBar的pointInside方法,将坐标convert到Button上,再调pointInside判断是否在Button上即可实现越界点击。
事件首先传递给最佳响应者(hit-tested view)响应,并在响应链中的传递。
响应过程
UIApplication通过Event中的Touch对象找到触摸所属的Window,将事件通过sendEvent传递给Window,Window也通过sendEvent将事件优先传递给最佳响应者(直接传递,因为此时Window已找到最佳响应者并保存了起来),然后通过 nextResponder 方法获取下一个响应者,形成响应链。
注:UIView的nextResponder是VC(如果此View是根视图)或其父视图;
VC的nextResponder是Window(如果此VC是根视图),如果VC是被present出来的,则是它的presenting view controller;
UIWindow的nextResponder是UIApplication;
UIApplication的nextResponder是AppDelegate。
事件的响应
UIResponder对象通过4个touch方法响应触摸事件,但默认不做事情,只单纯的继续传递,重写方法可以截获事件,进行 *** 作。(如创建自定义视图,重写touchMoved方法,通过touches和event属性实现简单的视图拖动)
注:在寻找最佳响应者时,所属的window和view会绑定到touch对象上,以供事件的传递过程中找到视图,给响应者发送事件。
事件的拦截
拦截和响应都是通过touchBegan方法控制的,默认实现为将事件沿着响应链继续向下传递。
1、不拦截,默认继续传递。
2、拦截,不再传递:重写touchesBegan处理事件,且不调用父类的方法。
3、拦截,继续传递:重写处理并调用父类的touchesBegan。
注:UIScrollViewDelayedTouchesBeganGestureRecognizer属性和手势延迟015秒的属性类似,所以,当视图中有手势和TableView共存时1、轻点手势只会执行手势而事件不会到达最佳响应者;2、短按会超过015到达最佳响应者但会因为手势拦截事件而被cancel;3、长按会因为手势识别失败,事件传递给最佳响应者执行cell的selected。
1、UIWindow是分发事件和响应事件的重要角色,是事件的起点:寻找最佳响应者时从Window开始寻找子视图,找到后也是由Window通过sendEvent:将事件传递给最佳响应者(hit-tested view)。
2、响应链中,Control上没有Gesture优先响应Control,Responder优先级最低,甚至会被视图层级低于自己的Gesture打断并Cancel掉Touch状态。
事件本身,type标识事件类型(触摸,加速计等),allTouchs属性包含了如多个手指产生的所有触摸对象(UITouch)的集合。
UITouch
源起触摸,封装在UIEvent内部,在事件传递时用于判断hitTest-view和确定GestureRecognizers。1手指1触摸生成1个UITouch;N手指1触摸生成N个UITouch对象;N手指N触摸,通过触摸位置判断是更新上次的还是再生成一个UITouch。手指离开屏幕一段时间后,确定UITouch不再更新才会释放。
UIResponder
响应者对象,具备响应事件的能力,因为其提供了4个处理触摸事件的Touch方法:Began、Moved、Ended、Cancelled,在接收到事件时调用,可以做出响应。如UIView/UIViewController/UIApplication/AppDelegate。
UIControl
以Target-Action模式处理触摸事件,如UIButton、UISwitch。UIControl跟踪到触摸事件时会向Target发送事件以执行Action(只接收单点触控)。跟踪的4个方法:beginTrackingWithTouch:(UITouch )touch、continueTracking、endTracking、cancelTracking。UIControl继承UIView,也具备普通UIResponder的身份,也有touch的4个方法,但默认实现与本类不同,如touchesBegan方法内部会调用beginTracking。
响应过程:先通过addTarget:action:forControlEvents:(UIControlEvents有许多关于交互事件的枚举)添加处理事件的target和action;当UIControl监听到事件时,sendAction把target、action和event都发给Application,再通过sendAction:from:to:forEvent向target发action(如果target为空,Application会在响应链中自己找能响应aciton的对象)
同样具有响应事件的能力,分为离散型(tap、swipe)和持续型。4个处理事件的方法跟UIResponder一样但无关(在UIGestureRecognizerSubclass中声明)。
手势识别成功后的处理: 事件响应在Source0回调的_UIApplicationHandleEventQueue()方法中,如果识别成了UIGesture手势时,首先调用Cancel打断当前的touch系列回调,然后将UIGestureRecognizer标记为待处理。当监听到BeforeWait事件时,回调函数_UIGestureRecognizerUpdateObserver()内部会获取所有标记待处理的手势,并执行手势回调。每当UIGestureRecognizer变化(创建销毁状态改变)时,RecognizerUpdate回调都会进行相应的处理。
离散型手势对响应链的影响
UIApplication在把事件传递给最佳响应者之前,会将事件先传递给相关的手势识别器开始识别,如果成功识别则会取消hit-tested view对事件的响应。但最佳响应者的touchBegan会先于手势的action执行,因为手势的识别成功(status = recognized)的时机是稍晚的。但重写手势的touchBegan方法即可证明UIWindow先把事件传递给了手势。-[TapGestureRecognizer touchesBegan:withEvent:]-[View touchesBegan:withEvent:]-[TapGestureRecognizer touchesEnded:withEvent:]Gesture Taped Action-[View touchesCancelled:withEvent:]注:如何优先传递给手势识别器?在hit-test过程中收集了UIGestureRecognizer的数组,保存在了event绑定的touch上。
持续型手势对响应链的影响
一个滑动的交互:pan手势识别过程中,连续事件会先传递给最佳响应者持续调用touchesMoved;pan手势识别成功后执行action,并通知Application去cancel掉响应者对事件的响应,之后都由手势接收事件并响应。若没有识别成功,则会一直传递给最佳响应者。
UIGestureRecognizer的三个属性轻点、短按、长按
cancelsTouchesInView:决定手势识别成功后,是否cancel响应链中响应者的响应(拦截并阻断),默认是YES
delaysTouchesBegan:是否在手势识别期间,就阻断事件传递给最佳响应者,默认是NO
delaysTouchesEnded:手势识别失败且触摸结束时,是否延迟015秒通知最佳响应者调用touchesEnded以结束事件响应,默认是YES
描述一下触摸事件的生命周期
分为系统响应阶段,和应用内部的传递、响应:IOKit到SpringBoard到APP(通过MachPort),应用内主线程RunLoop的Source回调中处理成UIEvent调UIApplication的sendEvent开始分发,通过hitTest找最佳响应者(传递链);响应者/手势识别器/Target-Action响应事件消耗或释放(响应链)。
系统是如何寻找最佳响应者的(传递链),如何在传递链中拦截事件
UIWindow中递归调用hitTest,判断三个交互条件+pointInside,满足则继续遍历子视图,否则返回自身。
重写pointInside可以扩大热区,拦截事件,自定义流向,最好不要调hitTest,因为实现不明,注意调super)
描述事件的响应过程(响应链),如何在响应链中拦截事件
事件由UIWindow先发送给最佳响应者,通过重写4个touch方法,截获处理后就终止(是否不再继续传递的标志是-是否调用supertouch方法),否则就nextResponder,期间手势识别器识别成功后会break掉touch的响应链。
UIGestureRecognizer对响应链的影响(离散型和持续型)
手势有识别过程,三个属性分别决定:识别成功后是否阻断响应链,是否开始识别时就阻断响应链,识别失败时候延迟通知最佳响应者调touchEnd。
所有继承响应者对象UIResponder都能接收并处理事件。按照时间顺序,先找到到最合适的view,然后就会调用view的touches方法,这些方法的默认做法是将事件顺着响应这链条向上传递,将事件交由上一个响应者进行处理,直到有一个view能处理该响应为止或者丢弃。这是一个从上到下,再从下到上的过程。
事件的传递先从父控件传递到子控件(UIApplication->window->寻找处理事件最合适的view)。
如果父view不能接受触摸事件,那么子view也不能接收到触摸事件。
有两个重要的方法:
view会调用hitTest:withEvent:方法,hitTest:withEvent:方法底层会调用pointInside:withEvent:方法判断触摸点是不是在这个view的坐标系上。如果在坐标系上,会分发事件给这个view的子view。然后每个字view重复以上步骤,直至最底层的一个合适的view。
事件响应会先从底层最合适的view开始,然后随着上一步找到的链一层一层响应touch事件。默认touch事件会传递给上一层。如果到了viewcontroller的view,就会传递给viewcontroller。如果viewcontroller不能处理,就会传递给UIWindow。如果UIWindow无法处理,就会传递给UIApplication。如果UIApplication无法处理,就会传递给UIApplicationDelegate。如果UIApplicationDelegate不能处理,则会丢弃该事件。
1发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的队列事件中
2UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow)
3主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件
事件的具体传递过程,如图:
一般事件的传递是从父控件传递到子控件的
例如:
点击了绿色的View,传递过程如下:UIApplication->Window->白色View->绿色View
点击蓝色的View,传递过程如下:UIApplication->Window->白色View->橙色View->蓝色View
如果父控件接受不到触摸事件,那么子控件就不可能接收到触摸事件
应用如何找到最合适的控件来处理事件?有以下准则
详述:
1主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
2如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
3遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1判断子控件能否接受事件,2点在不在子控件上)
4如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
UIView不能接收触摸事件的三种情况:
寻找最合适的view过程,如图:
这里点击了橙色的那块区域,事件传递判断过程如下:
1UIApplication从事件队列中取出事件分发给UIWindow
2UIWindow判断自己是否能接受触摸事件,可以
3UIWindow判断触摸点是否在自己身上,是的。
4UIWindow从后往前便利自己的子控件,取出白1(aUIWindow的子控件只有一个,那就是白1)
5白1都满足最上面两个条件,遍历子控件橙2
特别说明:
( a白1的子控件有两个,绿2和橙2 )
( b添加顺序是,先添加绿2,后添加橙2 )
( c根据后添加的子控件先遍历的原则,肯定是先遍历橙2子控件 )
6橙2都满足最上面两个条件,遍历子控件,先取出红3
( a橙2的子控件有两个,蓝3和红3,注意黄4不属于橙2的子控件而是蓝3的子控件 )
( b添加顺序是,先添加蓝3,后添加红3 )
( c根据后添加的子控件先遍历的原则,肯定是先遍历红3子控件 )
7红3不满足条件2,取出蓝3
8蓝3也不满足条件2,最后最合适的控件是橙2
寻找合适的View用到两个重要方法:
什么时候调用?
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法寻找合适的View
作用
寻找并返回最合适的view(能够响应事件的那个最合适的view)
注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,
事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
hitTest:withEvent:底层调用流程:
事件传递给窗口或控件后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。
以上是事件传递的顺序:
上文介绍了事件的传递过程,找到合适的View之后就会调用该view的touches方法要进行响应处理具体的事件,找不到最合适的view,就不会调用touches方法进行事件处理。
这里先介绍一下响应者链条:响应者链条其实就是很多响应者对象(继承自UIResponder的对象)一起组合起来的链条称之为响应者链条
一般默认做法是控件将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理 (即调用super的touches方法)。
那么如何判断当前响应者的上一个响应者是谁呢?有以下两个规则:
1判断当前是否是控制器的View,如果是控制器的View,上一个响应者就是控制器
2如果不是控制器的View,上一个响应者就是父控件
响应过程如下图:
touch响应:
如果控制器也不响应响应touches方法,就交给UIWindow。如果UIWindow也不响应,交给UIApplication,如果都不响应事件就作废了。
最后总结来说一次完整的触摸事件的传递响应过程为:
UIApplication-->UIWindow-->递归找到最合适处理的控件-->控件调用touches方法-->判断是否实现touches方法-->没有实现默认会将事件传递给上一个响应者-->找到上一个响应者-->找不到方法作废
一句话总结整个过程是:触摸或者点击一个控件,然后这个事件会从上向下(从父->子)找最合适的view处理,找到这个view之后看他能不能处理,能就处理,不能就按照事件响应链向上(从子->父)传递给父控件
额外添加:
有了响应网为基础,事件的传递就比较简单,只需要选择其中一条响应链,但是选择那一条响应链来传递呢?为了弄清真个过程,我们先来查看一下从触摸硬件事件转化为UIEvent消息。
首先用户触摸屏幕,系统的硬件进程会获取到这个点击事件,将事件简单处理封装后存到系统中,由于硬件检测进程和当前运行的APP是两个进程,所以进程两者之间传递事件用的是端口通信。硬件检测进程会将事件放入到APP检测的那个端口。
其次,APP启动主线程RunLoop会注册一个端口事件,来检测触摸事件的发生。当时事件到达,系统会唤起当前APP主线程的Runloop。唤起原因就是端口触摸事件,主线程会分析这个事件。
最后,系统判断该次触摸是否导致了一个新的事件, 也就是说是否是第一个手指开始触碰,如果是,系统会先从响应网中 寻找响应链。如果不是,说明该事件是当前正在进行中的事件产生的一个Touch message, 也就是说已经有保存好的响应链。
1、AppDelegatem自定义根视图
2、FirsteAppViewController的viewDidLoad方法,添加2个UIWindow
3运行,效果如下图所示:
4、GitHub源码:
该方式获取到的值是信号格数,并不是具体的信号值。如果需求只是来分析信号质量,可以采取这样的方法。具体的值目前还在研究中。 苹果真的是越来越防着我们知道具体的信号强度了。 难道以为我们不知道值就不知道他信号差了吗?(狗头)
if(@available(iOS130, )) {
NSArray arr = [UIApplication sharedApplication]connectedScenesallObjects;
UIWindowScenescene = arrfirstObject;
UIStatusBarManagerstatusBarManager = scenestatusBarManager;
id statusBar = nil ;
if ([statusBarManagerrespondsToSelector: @selector (createLocalStatusBar)]) {
UIViewlocalStatusBar = [statusBarManagerperformSelector: @selector (createLocalStatusBar)];
if ([localStatusBarrespondsToSelector: @selector (statusBar)]) {
statusBar = [localStatusBarperformSelector: @selector (statusBar)];
}
}
if (statusBar) {
id currentData = [[statusBarvalueForKeyPath:@"_statusBar"]valueForKeyPath:@"currentData"];
id cellularEntry = [currentDatavalueForKeyPath:@"cellularEntry"]; if ([cellularEntryisKindOfClass:NSClassFromString(@"_UIStatusBarDataIntegerEntry")]) {
signalStrength = [[cellularEntryvalueForKey:@"displayValue"]intValue];
}
}
}
UIApplication app = [UIApplication sharedApplication];
NSArraysubviews = [[[appvalueForKey:@"statusBar"]valueForKey:@"foregroundView"]subviews];
NSStringdataNetworkItemView = nil ;
for ( id subview in subviews) {
if ([subview isKindOfClass:[NSClassFromString(@"UIStatusBarSignalStrengthItemView") class]])
{
dataNetworkItemView = subview;
break ;
}
}
NSIntegersignalStrength = [[dataNetworkItemViewvalueForKey:@"signalStrengthRaw"]intValue];
NSStringsignalStrengthStr = [NSStringstringWithFormat:@"%lddBm",( long )signalStrength];
以上就是关于ios 怎么知道当前显示的viewcontroller全部的内容,包括:ios 怎么知道当前显示的viewcontroller、ios开发中怎么把全屏view加到window上面、iOS触摸事件等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)