事件傳遞:響應者鏈
當你設計一個app的時候,你很可能需要你的app能夠動態(tài)響應某些事件。比如,觸摸可以發(fā)生在屏幕上不同對象上,你需要決定哪些對象來響應一個特定的事件,并了解對象是如何接收事件。
當一個用戶事件產生的時候,UIKit
創(chuàng)建一個事件對象,這個對象包含了如何處理這個事件的信息。然后將這個事件對象放到這個活躍app的事件隊列中。對于觸摸事件,這些對象都到包裹成了UIEvent
對象。對于運動事件,取決于你使用的框架和你感興趣的運動事件的類型。
一個事件沿著一個特定的路徑,知道它被傳遞到一個能處理這個事件的對象。首先,事件最先被UIApplication
單例對象獲取,這個對象還負責這個對象的分發(fā)。典型的,這事件被傳遞到app的主窗口對象,也即使把時間傳遞給一個初始對象來處理。初始對象取決于事件的類型。
- 觸摸事件
對于觸摸事件,主窗口對象首先嘗試把事件傳遞給觸摸發(fā)生的視圖對象上。這個對象是一個hit-test
視圖對象。找到hit-test
視圖對象的過程叫hit-testing
。下面會詳細說明: - 運動事件和遠程事件
對于這兩類事件,主窗口對象把事件發(fā)送給第一響應者。
最終的目標是找到一個對象能處理這個事件和響應這個事件。因此,UIkit對象首先把事件發(fā)送給最適合處理這個事件的對象。對于觸摸事件,這個對象是hit-test
視圖,對于其他的事件,這個對象是第一響應者。接下來的部分會詳細說明hit-test
視圖和 第一響應者是如何被決定的。
Hit-Testing 返回觸摸發(fā)生的視圖
iOS使用hit-testing
來尋找被觸摸的視圖。Hit-testing
包括檢查一個觸摸是否發(fā)生在相關的視圖對象上。如果是,就遞歸的去檢查這個視圖的所有子視圖。最底層視圖包含了觸摸點的視圖成為hit-test
視圖。在知道hit-test
視圖之后,事件交給這個視圖來處理。

說明:假設用戶觸摸了視圖E在上圖中。iOS通過以下檢查子視圖的順序來找到hit-test
視圖:
- 1.觸摸事件發(fā)生在視圖A的邊界內。所以檢測它的子視圖B和C。
- 2.觸摸沒有發(fā)生在B,但是發(fā)生在C內。所以檢測C的子視圖D和E。
- 3.觸摸沒有發(fā)生在D,但是發(fā)生在E內。
視圖E是包含觸摸點堆棧視圖上最底層的視圖。所以視圖E成為hit-test
視圖。
方法 hitTest:withEvent:
根據一個給定的CGPoint
和UIEvent
返回指定的hit-test視圖。hitTest:withEvent:
方法首先調用pointInside:withEvent:
開始,如果傳遞到hitTest:withEvent:
的點在這個視圖的bounds內,那么pointInside:withEvent:
返回YES
。繼而,在返回YES
的所有子view上遞歸調用hitTest:withEvent:
。
如果傳遞hitTest:withEvent:
的點不在視圖的邊界內,第一次調用pointInside:withEvent:
方法會返回NO
,這個點會被忽略,hitTest:withEvent:
方法返回nil
.如果一個子視圖返回NO
。那么這個子視圖的所有堆棧上的視圖全部被忽略。因為觸摸沒有發(fā)生在這個子視圖上,就更不可能發(fā)生在其子視圖的子視圖上了。這意味著如果一個子視圖超出了其父視圖的邊界,則超出邊界的部分是不會響應事件。這部分父視圖都無法接收時間,更不用說往下傳遞了。如果子視圖的clipsToBounds
屬性被設置為NO
。
注意:一個觸摸事件在其生命周期內,跟這個
hit-test
視圖綁定,即便這個觸摸事件滑出了當前視圖。
這個hit-test
視圖被給予第一優(yōu)先權處理這個觸摸事件。如果這個視圖不能處理這個事件,這個觸摸事件順著響應者鏈向上傳遞,直到找到第一個能處理這個事件的對象。
響應者鏈是由一系列響應者連成的鏈
很多類型的事件的傳遞都依賴于響應者鏈。響應者鏈是一系列連接在一起的響應者對象。它從第一響應者開始,以application
對象結束。如果第一響應者不能處理這個事件,它會把這個事件沿著這個響應者鏈傳遞到下一個響應者。
一個響應者對象是一個能響應并能處理事件的對象。UIResponder
類是所有響應者的父類,它定義了事件處理和常見響應者行為的通用編程接口。UIApplication
,UIViewController
以及UIView
類的實例對象都是響應者,這表明,所有視圖和絕大多數主控制器都是響應者。需要注意的是核心動畫的圖層對象不是響應者。
第一響應者被指定為首先接收事件的對象。通常,第一響應者是一個視圖對象。一個對象要成為第一響應者,需要滿足下面兩個條件:
- 1.重寫
canBecomeFirstResponder
方法并返回YES
。 - 2.接收
becomeFirstResponder
消息。如果有必要,一個對象可以給自己發(fā)送這個消息。
注意:在指定一個對象為第一響應者之前,要確保這個對象在其合適的生命周期內。比如,通常在重寫的
viewDidAppear:
方法中調用becomeFirstResponder
方法。如果你試圖在viewWillAppear:
給一個第一響應者賦值,由于對象還沒有創(chuàng)建完成。那么becomeFirstResponder
方法會返回NO
。
事件并不是唯一依賴于響應者鏈的對象。響應者鏈在以下處理中都會用到:
-
觸摸事件
如果hit-test
視圖不能處理這個觸摸事件,則從這個視圖開始順著響應者鏈向上級傳遞。 -
運動事件
UIKit
框架的對象如果要處理搖晃事件,第一響應者必須要實現UIResponder
類的motionBegin:withEvent:
或motionEnded:withEvent:
方法。 -
遠程事件
如果要處理遠程事件,第一響應者比如要實現UIResponder
類的remoteControlReceivedWithEvent:
方法。 -
Action消息
當用戶在操縱一個控件時,例如一個按鈕或者一個開關,這個action
方法的target
為空,這個消息會順著響應者鏈從第一響應者(可能是這個控件自身)往后傳遞。 -
快捷菜單消息
當用戶點擊了快捷菜單的菜單時,iOS使用響應者鏈來找到一個實現了必要方法的對象來處理。(實現cut:
,copy:
,paste:
) -
文本編輯
當用戶點擊一個textField
或者textView
,則這個文本控件自動變成第一響應者。默認的響應是彈出虛擬鍵盤,這個文本控件獲得焦點,可以開始編輯。你還可以根據應用的需要自定義鍵盤。你還可以給任何響應者對象添加自定義的輸入控件。
UIKit
自動將用戶點擊的文本控件設置為第一響應者,其他對象,則需要顯示的調用becomeFirstResponder
方法來成為第一響應者。
響應者鏈的路徑
如果初始對象,hit-test
視圖或者第一響應者不能處理事件。UIKit
把事件傳遞給響應者鏈上的下一個響應者。每一個響應者決定是否處理這個事件或者是把這個事件繼續(xù)傳遞給下一個響應者,使用nextResponder
方法。直到找到一個響應者或再也沒有別的響應者。一個響應者序列,從iOS檢測到事件并傳遞給初始對象開始,通常是一個視圖。這個初始視圖有第一優(yōu)先權來處理這個事件。下圖顯示了兩種事件傳遞路徑對于不同的應用配置。一個應用的事件傳遞路徑由其具體的結構決定,但遵循著同樣的規(guī)律。

對于左圖,事件傳遞路線
- 1.
initial view
試圖處理這個事件或消息。如果它不能處理這個事件,它把事件傳遞給它的父視圖,因為initial view
不是控制器的根視圖。 - 2.父視圖試圖處理這個事件。如果它也不能處理這個事件,則繼續(xù)傳遞給它的父視圖,因為當前視圖還不是控制器的根視圖。
- 3.控制器的根視圖試圖處理這個事件,如果根視圖還不能處理這個事件,則把事件傳遞給它的控制器。
- 4.控制器視圖處理這個事件,如果不能,則把事件傳遞給窗口對象。
- 5.如果窗口對象還不能處理它,則把事件傳遞給單例的
application
對象。 - 6.如果單例的
application
對象也不能處理,則事件被忽略。
右邊的情形有點稍微的不同,但都遵循著以下規(guī)律:
- 1.事件向上傳遞,直到遇到某個控制器的根視圖。
- 2.根視圖把事件傳遞給它的控制器。
- 3.控制器接著把事件傳遞給包含它的視圖,一直傳遞到另一層根視圖。1~3步重復,直到找到根控制器。
- 4.根控制器把事件傳遞給窗口對象。
- 5.窗口對象把事件傳遞給
application
對象。
重要:如果你實現一個自定義視圖來處理遠程事件,
action
消息,UIKit
運動事件,快捷菜單消息。不要直接把事件或者消息傳遞給下一個響應者。取而代之的是,你應該先調用父類的實現,讓UIKit
為你處理響應者鏈的遍歷。