既然是一個Bug引發(fā)的思考,自然要先上Bug,如上動圖所示,在輸入了空格標題之后,引發(fā)一個問題,就是光標依然在文本框內,再敲擊鍵盤依然可以輸入改變文本框。
gif不太好看,再來看下截圖
本人本著不服輸?shù)木駥Υ藛栴}進行了深入研究,結果引發(fā)出來三個新的知識點,分別是:
①:光標還在文本框內,并不是NSALert窗口引發(fā)的Bug,但是NSAlert確實會打斷當前RunLoop循環(huán)內的事件傳遞響應鏈,同時還會影響本次循環(huán)內后續(xù)的部分UI更新功能,比如說,約束!。重要強調一下,是部分,不是所有!!!這個問題可以用下面的知識點③來解決,當然還有其它辦法,詳見
②:光標還在文本框內,原因是resignFirstResponder并不能真正辭掉NSTextField的第一響應者身份(這點跟iOS不同)。而且,當NStextField在編輯狀態(tài)中時,設置editable屬性為NO,能讓NSTextField變?yōu)椴豢删庉嫞仨毷切枰诋斍熬庉嬐瓿芍蟛趴梢裕乱淮蔚木庉嬑谋究騼热莶挪槐徽埱蟆<词惯@兩個方法同時被調用,也不能結束當前NSTextField的編輯狀態(tài),需要調用NSTextField的abortEditing方法來結束編輯狀態(tài)
③:當調用performSelectorOnMainThread: withObject: waitUntilDone:方法,當最后一個參數(shù)傳YES時,不僅僅會阻塞當前線程,而且會阻塞當前線程上的當前RunLoop循環(huán)。傳NO,則分兩種情況,一是在主線程上調用,會阻塞主線程,但不會阻塞當前RunLoop循環(huán);二是在子線程上調用,什么都不會阻塞。
===========================我是分割線===========================
枯燥的文字描述已經結束,下面就來講述我們的探究歷程:
先奉上這段問題代碼:
'''
-
(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]; //當點擊區(qū)域在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引發(fā)的,所以把彈框和辭掉響應者的代碼位置調整了一下,然后變成先辭掉第一響應者,再彈窗,下面請看問題Gif
先辭掉第一響應者后彈窗.gif
再來看下截圖
我去,這下問題更大了,不僅光標沒去掉,還可以接受文本編輯事件,NSTextField更是沒有自適應大小!你妹啊。
沒辦法,自好再去考慮其他的問題,想來想去,可能是因為我這段代碼是在錨點事件回調內部產生的,所以可能會受到點影響,于是就去外面寫了一個Demo(代碼特別簡單,就不再奉上了)測試下,結果意外發(fā)現(xiàn),在沒有NSAlert彈窗和錨點事件回調的影響的情況下,NSTextField依然沒有辭掉第一響應者身份,光標也依然在,文本也在改變。蒼天啊,大地啊,resignFirstResponder不好使了嘛?我以前在iOS里都是這么調的啊。沒辦法,只好去翻NSTextField的頭文件,不出意外,在仔仔細細看了一整遍NSTextField.h文件后,依然一無所獲,在此有種想哭的感覺。然后看NSTextField繼承,結果在NSControl.h中發(fā)現(xiàn)了一個 abortEditing 方法。如或至寶啊,那就拿過來試試,恩,果然好使。
好了,問題解決了,然后經過把下面三個方法各種組合,在NSTextField各種狀態(tài)下縝密測試,得出下面結論:
1、resignFirstResponder:確實沒有使NSTextField辭掉第一響應者身份
2、setEditable:設置editable屬性為NO,也確實能讓NSTextField變?yōu)椴豢删庉嫛5O置是,必須確保NSTextField不再編輯狀態(tài)下,否則當前還是可以繼續(xù)編輯文本框內容的,下一次的編輯文本框內容才不被接受。
3、abortEditing:強制NSTextField退出文本編輯狀態(tài),但是有一個Bug,那就是拼音輸入漢字的時候,如果沒有點空格就調用此方法,會把一堆字母加單引號輸進去,目前除了去controlTextDidEndEditing里做攔截,還真沒想到其它好的方法。
在剛總結完上面結論的時候,發(fā)現(xiàn)了第二個問題,那就是我的點擊事件被攔截了!!!來,先看看Gif,確定下發(fā)生了什么事情:
當我在點擊好友,以此來觸發(fā)彈窗的時候,我點擊好友竟然沒效果!!!有彈窗,有錨點事件,斷點調試不太友好,所以只好苦逼的去打Log,結果發(fā)現(xiàn)event被return出去了,也就是說我的代碼正常被執(zhí)行了,卻沒出來該有的效果。
涉及到這方面的問題,我第一個想到的就是RunLoop,姑且算是死馬當活馬醫(yī)吧,于是也用了一個死馬當活馬醫(yī)的嘗試,就是用performSelectorOnMainThread:withObject:waitUntilDone:函數(shù)回調到主線程來彈窗。在主線程里回調主線程去拋彈窗,腦子有問題吧,O__O "…就當是吧,反正試一試又不會懷孕,萬一要是解決問題了呢?
恩,還真就解決問題了呢!來,再看gif:
好了,問題是解決了,我們來分析下,為什么回調主線程就能解決問題呢?
二、關于模態(tài)窗口打斷事件傳遞響應鏈的問題分析
首先,我們找到蘋果官方文檔中對于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彈出的是一個模態(tài)窗口。但是這種窗口老霸道了,當它啟動以后,僅它自己可以接收和相應用戶操作,無法切換到其他窗口操作,其他窗口也不能接收處理系統(tǒng)內部各種事件。同時,如果在你彈出模態(tài)窗口的的RunLoop循環(huán)內,當前的系統(tǒng)事件沒有傳遞完的話,也會被打斷,無法繼續(xù)傳遞下去,但是,關于這,貌似有一個小Bug,請看Gif。
好了,既然說明了問題所在,那我們就來說明為什么這么做就能解決問題:
performSelectorOnMainThread:withObject:waitUntilDone:當在在主線程中調用此函數(shù)的時候,①如過末尾參數(shù)傳遞YES;則不僅僅會阻塞線程,還會阻塞當前RunLoop循環(huán)。這樣就相當于把回調的selector方法里的代碼放到當前位置執(zhí)行,這樣就只有一個結果,那就是沒任何效果。換成白話講就是,在代碼中標記1到標記3的執(zhí)行順序是:標記1→標記3→標記2,而且這三處代碼在同一個RunLoop循環(huán)內執(zhí)行。
②如果末尾參數(shù)傳NO,則會產生另外一種代碼執(zhí)行順序:標記1→標記2→標記3。客官,請注意,當前改變的不僅僅是代碼的執(zhí)行順序,還有另外一層更重要的改變,就是在執(zhí)行完標記2代碼后,并不是立刻執(zhí)行標記3的代碼,而是等到下一次RunLoop循環(huán)的時候才會由系統(tǒng)調用執(zhí)行標記3的代碼。說到這里就已經很明白了,由于彈出Alert窗口的RunLoop循環(huán)已經不再是原來的循環(huán),等及彈窗的時候原來的循環(huán)早已執(zhí)行完畢,用戶的點擊事件也早已在那個循環(huán)內被傳遞并且響應完畢,故就能解決上述問題。
③上述問題還有多種解決方案,那就是多線程,具體做法就是在主線程內開啟異步回調主線程進行彈框,至于原理,跟上述一樣,都是利用代碼的執(zhí)行時間差來解決問題,這或許是另外的一種“異步”吧。