既然是一个Bug引发的思考,自然要先上Bug,如上动图所示,在输入了空格标题之后,引发一个问题,就是光标依然在文本框内,再敲击键盘依然可以输入改变文本框。
先弹窗后辞掉第一响应者.gif
gif不太好看,再来看下截图
光标仍然存在.jpg
本人本着不服输的精神对此问题进行了深入研究,结果引发出来三个新的知识点,分别是:
①:光标还在文本框内,并不是NSALert窗口引发的Bug,但是NSAlert确实会打断当前RunLoop循环内的事件传递响应链,同时还会影响本次循环内后续的部分UI更新功能,比如说,约束!。重要强调一下,是部分,不是所有!!!这个问题可以用下面的知识点③来解决,当然还有其它办法,详见
②:光标还在文本框内,原因是resignFirstResponder并不能真正辞掉NSTextField的第一响应者身份(这点跟iOS不同)。而且,当NStextField在编辑状态中时,设置editable属性为NO,能让NSTextField变为不可编辑,但必须是需要在当前编辑完成之后才可以,下一次的编辑文本框内容才不被请求。即使这两个方法同时被调用,也不能结束当前NSTextField的编辑状态,需要调用NSTextField的abortEditing方法来结束编辑状态
③:当调用performSelectorOnMainThread: withObject: waitUntilDone:方法,当最后一个参数传YES时,不仅仅会阻塞当前线程,而且会阻塞当前线程上的当前RunLoop循环。传NO,则分两种情况,一是在主线程上调用,会阻塞主线程,但不会阻塞当前RunLoop循环;二是在子线程上调用,什么都不会阻塞。
===========================我是分割线===========================
枯燥的文字描述已经结束,下面就来讲述我们的探究历程:
先奉上这段问题代码:
'''
-
(IBAction)editTitleBtnClick:(id)sender {
self.titleLabel.editable = YES;
[self.titleLabel becomeFirstResponder];__weak typeof(self) weakSelf = self;
NSString *oldTitle = self.titleLabel.stringValue;self.mEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask | NSLeftMouseDraggedMask | NSLeftMouseUpMask handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
NSPoint p = [event locationInWindow]; NSPoint newP = [weakSelf.titleLabel convertPoint:p fromView:nil]; //当点击区域在TextField外时 if (!CGRectContainsPoint(weakSelf.titleLabel.bounds, newP)) { [NSEvent removeMonitor:self.mEventMonitor]; weakSelf.mEventMonitor = nil; NSString *newTitle = [weakSelf.titleLabel.stringValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (newTitle.length <= 0){ weakSelf.titleLabel.stringValue = oldTitle; [YHNAlertWindow alertToShowMessageWithButtonTitle:@"确定" AndMessageText:@"请输入分组名称"];
// [weakSelf performSelectorOnMainThread:@selector(showMessage) withObject:nil waitUntilDone:NO]; //标记1
}else if(![oldTitle isEqualToString:newTitle]){
[weakSelf editFinishedBtnClick];
}
[weakSelf.titleLabel resignFirstResponder];
weakSelf.titleLabel.editable = NO;
//回复文本框为Label
return event;//标记2
}
return event;
}];
}
-
(void)showMessage{
[YHNAlertWindow alertToShowMessageWithButtonTitle:@"确定" AndMessageText:@"请输入分组名称"];//标记3
}
'''
一、关于NSTextField辞去第一响应者的问题
首先一开始,确实以为NSTextField不能正常辞掉第一响应者身份是NSAlert引发的,所以把弹框和辞掉响应者的代码位置调整了一下,然后变成先辞掉第一响应者,再弹窗,下面请看问题Gif
先辞掉第一响应者后弹窗.gif
再来看下截图
先辞掉第一响应者后弹窗2.jpg
我去,这下问题更大了,不仅光标没去掉,还可以接受文本编辑事件,NSTextField更是没有自适应大小!你妹啊。
没办法,自好再去考虑其他的问题,想来想去,可能是因为我这段代码是在锚点事件回调内部产生的,所以可能会受到点影响,于是就去外面写了一个Demo(代码特别简单,就不再奉上了)测试下,结果意外发现,在没有NSAlert弹窗和锚点事件回调的影响的情况下,NSTextField依然没有辞掉第一响应者身份,光标也依然在,文本也在改变。苍天啊,大地啊,resignFirstResponder不好使了嘛?我以前在iOS里都是这么调的啊。没办法,只好去翻NSTextField的头文件,不出意外,在仔仔细细看了一整遍NSTextField.h文件后,依然一无所获,在此有种想哭的感觉。然后看NSTextField继承,结果在NSControl.h中发现了一个 abortEditing 方法。如或至宝啊,那就拿过来试试,恩,果然好使。
好了,问题解决了,然后经过把下面三个方法各种组合,在NSTextField各种状态下缜密测试,得出下面结论:
1、resignFirstResponder:确实没有使NSTextField辞掉第一响应者身份
2、setEditable:设置editable属性为NO,也确实能让NSTextField变为不可编辑。但设置是,必须确保NSTextField不再编辑状态下,否则当前还是可以继续编辑文本框内容的,下一次的编辑文本框内容才不被接受。
3、abortEditing:强制NSTextField退出文本编辑状态,但是有一个Bug,那就是拼音输入汉字的时候,如果没有点空格就调用此方法,会把一堆字母加单引号输进去,目前除了去controlTextDidEndEditing里做拦截,还真没想到其它好的方法。
在刚总结完上面结论的时候,发现了第二个问题,那就是我的点击事件被拦截了!!!来,先看看Gif,确定下发生了什么事情:
阻挡事件传递.gif
当我在点击好友,以此来触发弹窗的时候,我点击好友竟然没效果!!!有弹窗,有锚点事件,断点调试不太友好,所以只好苦逼的去打Log,结果发现event被return出去了,也就是说我的代码正常被执行了,却没出来该有的效果。
涉及到这方面的问题,我第一个想到的就是RunLoop,姑且算是死马当活马医吧,于是也用了一个死马当活马医的尝试,就是用performSelectorOnMainThread:withObject:waitUntilDone:函数回调到主线程来弹窗。在主线程里回调主线程去抛弹窗,脑子有问题吧,O__O "…就当是吧,反正试一试又不会怀孕,万一要是解决问题了呢?
恩,还真就解决问题了呢!来,再看gif:
完美.gif
好了,问题是解决了,我们来分析下,为什么回调主线程就能解决问题呢?
二、关于模态窗口打断事件传递响应链的问题分析
首先,我们找到苹果官方文档中对于NSAlert的描述,有这么一段话
'''
An alert appears onscreen either as an app-modal dialog or as a sheet attached to a document window. The methods of the NSAlert class allow you to specify alert level, alert text, button titles, and a custom icon should you require it. The class also lets your alerts display a help button and provides ways for apps to offer help specific to an alert.
'''
很长的描述,但是对我们有用的只有第一句话,就是An alert appears onscreen either as an app-modal dialog or as a sheet attached to a document window.这句,用我的理解翻译过来就是,NSAlert弹出的是一个模态窗口。但是这种窗口老霸道了,当它启动以后,仅它自己可以接收和相应用户操作,无法切换到其他窗口操作,其他窗口也不能接收处理系统内部各种事件。同时,如果在你弹出模态窗口的的RunLoop循环内,当前的系统事件没有传递完的话,也会被打断,无法继续传递下去,但是,关于这,貌似有一个小Bug,请看Gif。
好了,既然说明了问题所在,那我们就来说明为什么这么做就能解决问题:
performSelectorOnMainThread:withObject:waitUntilDone:当在在主线程中调用此函数的时候,①如过末尾参数传递YES;则不仅仅会阻塞线程,还会阻塞当前RunLoop循环。这样就相当于把回调的selector方法里的代码放到当前位置执行,这样就只有一个结果,那就是没任何效果。换成白话讲就是,在代码中标记1到标记3的执行顺序是:标记1→标记3→标记2,而且这三处代码在同一个RunLoop循环内执行。
②如果末尾参数传NO,则会产生另外一种代码执行顺序:标记1→标记2→标记3。客官,请注意,当前改变的不仅仅是代码的执行顺序,还有另外一层更重要的改变,就是在执行完标记2代码后,并不是立刻执行标记3的代码,而是等到下一次RunLoop循环的时候才会由系统调用执行标记3的代码。说到这里就已经很明白了,由于弹出Alert窗口的RunLoop循环已经不再是原来的循环,等及弹窗的时候原来的循环早已执行完毕,用户的点击事件也早已在那个循环内被传递并且响应完毕,故就能解决上述问题。
③上述问题还有多种解决方案,那就是多线程,具体做法就是在主线程内开启异步回调主线程进行弹框,至于原理,跟上述一样,都是利用代码的执行时间差来解决问题,这或许是另外的一种“异步”吧。
网友评论