1. 程式人生 > >UI控制元件無法響應點選等事件的探索

UI控制元件無法響應點選等事件的探索

轉自:hcios.com

一、響應者鏈

關於響應者鏈,有如下一段介紹:每一個應用有一個響應者鏈,我們的檢視結構是一個N叉樹(一個檢視可以有多個子檢視,一個子檢視同一時刻只有一個父檢視),而每一個繼承UIResponder的物件都可以在這個N叉樹中扮演一個節點。當葉節點成為最高響應者的時候,從這個葉節點開始往其父節點開始追朔出一條鏈,那麼對於這一個葉節點來講,這一條鏈就是當前的響應者鏈。響應者鏈將系統捕獲到的UIEvent與UITouch從葉節點開始層層向下分發,期間可以選擇停止分發,也可以選擇繼續向下分發。

那,我要是告訴你們,響應者鏈就是上面那段話介紹的,估計你們得拿板磚拍我了。這等於沒說。別急,先來舉個栗子:

我用SingleView模板建立了一個新的工程,它的主Window上只有一個UIViewController,其View之上有一個Button。這個專案中所有UIResponder的子類所構成的N叉樹為這樣的結構:

b26Nri.jpg!web

那麼他看起來並不像N叉樹,但是不代表者不是一顆N叉樹,當我們專案複雜之後,這個View可不可以有多個UIButton節點?所以他就是一棵樹。

實際上我們要把這棵樹寫完整,應該還要算上UIButton的UILabel和UIImageView,因為他們也是UIReponder的子類。這裡先不考慮了。

所以我們嘗試在任意地方列印這個Button的nextReponder物件。nextResponder物件是UIReponder類的例項方法,它會返回任意物件在樹中的上一個響應者例項:

  1. NSLog(@"%@",self.btn.nextResponder);

控制檯輸出資訊如下:

<UIView: 0x7f8973e92b60; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f8973e92ee0>>

咱們可以一直列印下去,獲取下一個Responder的下一個Responder,依次獲取的響應者是:

<ViewController: 0x7f8973c2ccf0>

(null)

為什麼這裡ViewController沒有父親呢?

注意這句程式碼我是寫在ViewDidLoad中,而我們知道這個方法的生命週期比較早,所以我們換個地方寫或者延遲一段時間再列印,兩種方法都可以得到結果(由此可以推理出我們響應者樹的構造過程是在ViewDidLoad週期中來完成的,這個函式會將當前例項的構成的響應者子樹合併到我們整個根樹中):

  1. double delayInSeconds =2.0;
  2. dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,(int64_t)(delayInSeconds * NSEC_PER_SEC));
  3. dispatch_after(popTime, dispatch_get_main_queue(),^(void){
  4. NSLog(@"%@",self.btn.nextResponder.nextResponder.nextResponder);
  5. NSLog(@"%@",self.btn.nextResponder.nextResponder.nextResponder.nextResponder);
  6. NSLog(@"%@",self.btn.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
  7. NSLog(@"%@",self.btn.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
  8. });

最後獲取就是上邊給出的那幅圖的樣子了。那說了這麼多,這個Responder到底有什麼用呢?

在AppDelegate裡面重寫touchesBegan方法:

  1. -(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
  2. {
  3. NSLog(@"AppDelegate接收到觸控事件");
  4. }

在ViewController裡面也重寫:

  1. -(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
  2. {
  3. [super touchesBegan:touches withEvent:event];
  4. NSLog(@"ViewController接收到觸控事件");
  5. }

使用者手指觸控到了UIView上,由於我們沒有重寫UIView的UITouchEvent,所以他裡面和super執行的一樣的,將該事件繼續分發到UIViewController;

UIViewController的TouchBegan被我們重寫了,如果我們不super,那麼我們在這裡寫響應程式碼。事件到這裡就不繼續分發了。可想而知,UIViewController祖先節點:UIWindow,UIApplication,AppDelegate都無權被分發此事件。

如果我們super了TouchBegan,那麼此次觸控事件由

ViewController分發給UIWindow,

UIWindow繼而分發給UIApplication,

UIApplication再分發給AppDelegate,

於是我們在ViewController和appDelegate的touchBegan方法中都捕獲到了這次事件。

但是這只是處理點選事件順序,也即是確認了第一響應者之後的處理流程。

那尋找第一響應者的流程是怎樣的呢。

其實就是逆向走一遍,當沿著這條響應者鏈找到了第一響應者,那麼就會返回事件給自己的nextResponder去處理,直到appDelegate。

下面回到咱們的主題,既然點選按鈕,沒有觸發點選事件,響應者鏈有很大嫌疑。

為什麼這麼說呢??

我問大家,如果你點選完按鈕之後,沒有找到第一響應者,或者是第一響應者找錯了,還會呼叫button的觸發事件嗎??答案顯而易見。

下面列舉常見的幾大原因,有興趣的同學可以去試試。

  • 按鈕不在響應者鏈上(比如我遇到的按鈕從螢幕外推入螢幕內,不響應點選事件,初步猜想是button初始位置在UIWindow外,所以不在響應者鏈上)
  • 按鈕的點選事件被其他控制元件攔截(比如按鈕上面有按鈕,點選上面的按鈕)

二、檢視生命週期

這個理解起來就簡單一些,比如實現按鈕點選事件所在的檢視控制器被回收了。但是由於按鈕被新增到當前可見檢視上,按鈕沒有被回收,所以是可見的。但是點選這個按鈕卻沒有任何作用。

三、總結

單單是上面兩個方面並不能涵蓋所有的類似情況,而且這節咱們只是從UIButton入手。在具體的開發中,有很多經驗所不能夠解釋的現象,但是上面兩點一般是優先考慮的。