喬幫主在發布會上提到,用戶的手才是最好的輸入設備,的確,iPhone之后,非觸屏手機再已難覓。觸摸是最基本的用戶輸入事件,理解iOS特有的觸摸事件響應機制,能夠良好管理程序中觸摸響應方法,避免沖突的發生。
iOS中的事件
iOS中的事件主要分為三類:
- UIControl Actions: 使用target/action注冊的SEL。
- User Events: 用戶與應用之間的交互:觸摸,輸入文字,搖晃,遠程控制等。
- System Events: 應用啟動,切前后臺,低內存等。
cocoa和cocoa touch的程序啟動后,,會首先初始化一些基本資源:在主線程創建一個main event loop;初始化主UIWindow
。
應用啟動過程-w500
main event loop本質上是一個NSRunLoop
,與其他輔助線程的run loop不同,其是自創建后自動開始運行的。主消息循環最大的特點是:它在創建時就與負責捕獲用戶事件的系統底層建立了連接,所以它的input source可以收到系統傳遞過來的用戶事件。UIApplication
對象會將當前要處理的用戶事件封裝成UIEvent
,發送給UIWindow
,在由UIWindow
轉發給對應的響應者。
iOS響應用戶事件
UIEvent
表示用戶與iOS產生交互的事件,UIWindow
將觸摸事件發送給hitTest View,其他事件發送給first responder,若它們不能處理該事件,事件在響應鏈向上傳遞,找到最終的響應者或丟棄。
本文主要介紹觸摸事件的響應機制。
iOS中能夠捕獲觸摸事件的類
iOS程序中,有三種類可以接受用戶的觸摸事件并響應,分別是:UIControl
, UIReponder
, UIGestureRecognizer
,這三個類在參與觸摸響應機制的時機不同,在實際使用時要加以注意。
iOS中的觸摸事件
iOS中使用UItouch
來表示用戶的一根手指在屏幕上的觸摸行為。當用戶觸摸屏幕時,硬件會捕捉到觸摸行為,將觸摸點的半徑、力度和坐標等發送給iOS,經過UIKit
封裝后,得到UITouch
對象。通過UITouch
對象,我們可以獲得其關聯的視圖(hitTest View),在視圖中的坐標,生命周期的當前階段,點擊數等信息。。一次用戶點擊多次的事件,其只包含一個UITouch
觸摸類型的UIEvent
包含至少一個UITouch
,也就是用戶在屏幕上的一次手勢操作的手指運動,其會持有此次事件相關聯的UITouches
序列。,即在一次手勢操作中,其中一個手指中途離開屏幕,它所對應的UITouch
依然存在于該事件中。響應者會在touchesBegan:withEvent:
等方法中獲取UITouch
對應的UIEvent
。
UITouches
序列在用戶第一根手指觸摸屏幕時開始,最后一根手指離開時結束,當手指狀態變化時,iOS會將序列中的UITouch
對象發送給UIEvent
對象。

iOS的觸摸事件響應機制
當用戶觸摸屏幕時,對應的觸摸事件會加入到UIApplication
事件隊列中,當下一個RunLoop來臨時,UIApplication
會將出列最前端的事件,發送給當前的UIWindow
(key window
)。
UIWindow
會調用hitTest:withEvent:
方法,開始hit-testing流程尋找包含觸摸點的視圖。該流程會返回包含觸摸點的層級最低的視圖。
每當用戶觸摸屏幕時,UIKit
都會執行hit-testing,之后再從hitTest視圖開始尋找事件的響應者。當hitTest視圖決定后,它就關聯了對應的觸摸事件,會持續收到觸摸事件生命周期的方法,(touchBegan, touchMove, touchCancel/touchEnd),即使是觸摸點已經在touchMove階段移出了hitTest視圖,它依然能夠收到后續的消息。
Note: A touch object is associated with its hit-test view for its lifetime, even if the touch later moves outside the view.
hit-testing流程
iOS中hit-testing使用逆前序的深度遍歷算法來確定用戶點按的最低層級(最靠近用戶)的視圖,該hitTest視圖是觸摸事件的響應鏈頭結點。
逆前序的深度遍歷算法:根節點-->右子樹-->左子樹。
當收到觸摸事件后,UIApplication
在當前視圖層級中,從key window
開始(最頂級),從上往下遍歷子視圖調用hitTest:withEvent:
,若找到hitTest視圖則停止遍歷并返回。
當視圖收到hitTest:withEvent:
方法后,通過下列條件判斷是否在該視圖執行hit-testing。
-
pointInside:withEvent:
方法返回YES。pointInside:withEvent:
方法用來判斷觸摸點是否在當前視圖內。 - hidden == NO。
- userInteractionEnabled == YES。
- alpha >= 0.01。若view的content繪制為透明的,則不受影響。
需要注意的是,當clipsToBounds == NO時,視圖的子視圖可能會超出其bounds,這種情況如果觸摸點在子視圖超出父視圖的范圍,那么hit-tesing不會再此視圖樹上執行。
如圖,當用戶觸摸
viewB.1
時,UIApplication
對象收到觸摸事件,從key window
開始執行hit-testing,首先訪問viewC
,由于pointInside:withEvent:
方法返回NO,取消執行并訪問viewB
,滿足執行,則從右往左開始訪問其子視圖(視圖層級從下往上),找到viewB.1
,它沒有子視圖,則返回自己。最終UIWindow
對象將viewB.1
作為hitTest視圖返回給UIApplication
對象。可以看到,當某一視圖收到
hitTest:withEvent:
方法后,它會向所有子視圖發送hitTest:withEvent:
方法,若它的沒有子視圖或所有子視圖返回nil,那么就返回自己,所有hit-testing流程最終一定會找到一個對象UIView/UIWindow
去接收觸摸事件。以下是
hitTest:withEvent:
可能的實現。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
responder chain
responder chain是UIResponder
對象組成的鏈形結構,它以first responder為頭結點,UIApplication
對象為尾節點,事件從頭開始在響應鏈中向上傳遞。
UIResponder
用來設計處理事件,UIApplication
, UIViewController
, UIView
都是其子類,只要它們實現了UIResponder
中的鉤子方法,就可以響應對應的事件。

其中
first responder
用來第一個接觸事件,可以使用becomeFirstResponder
來設置它,主要要在視圖層級已經完全建立之后再設置。
If you try to assign the first responder in viewWillAppear:, your object graph is not yet established, so the becomeFirstResponder method returns NO
默認情況下,fist responder
是當前UIWindow
中最有可能響應事件的UIView
,這由UIkit
決定。
iOS中大部分的事件都依賴響應鏈來找到最終的響應者,在UIResponder
的頭文件中可以看到,Touch events,Motion events,Remote events,UIControl Action,Text editing,press events等事件都可以在響應鏈中傳遞。
尋找響應對象
當UIApplication
在處理的事件時,觸摸事件會交給hitTest view
開始的響應鏈處理,其他的動作事件,遠程事件,系統事件等,會交給first responder
開始的響應鏈處理。
UIKit會將用戶事件發送給理論上最合適的對象。所以當程序中的響應者要經過很長的查找路徑時,這時就要考慮是否實現是否設計合理了。
UIKit first sends the event to the object that is best suited to handle the event. For touch events, that object is the hit-test view, and for other events, that object is the first responder
對于觸摸事件,hit-test視圖獲得了最先接受觸摸對象的機會,但如果它不能處理對應的觸摸事件,那么UIKit會沿著以hit-test開頭的響應鏈尋找能夠最終的響應者。

當找到響應者或已經到鏈尾(UIApplication)仍不能處理,UIKit會停止查找,對于后者,對應的事件會被丟棄。
除了UIResponder
對象,UIGestureRecognizer
與UIControl
也可以響應觸摸事件,但它們參與觸摸事件響應的方式不同。
-
UIGestureRecognizer
在響應鏈中的位置取決于依附的視圖。 -
UIControl
參與響應的方式決定于其關聯的target。
UIGestureRecognizer
要先于視圖收到觸摸事件,但需要注意的是,若該視圖也可以響應觸摸事件(實現了UITouch
生命周期函數),那么手勢對象并不會阻礙視圖的響應,雙方是同時響應的,只不過存在先后順序。
UIGestureRecognizer與UIView的接觸事件的次序
響應觸摸事件
當確定了響應鏈后,UIWindow
會向hitTest View
發送以下方法:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
這是UIResponder
用于響應觸摸事件的方法,這些鉤子方法的默認實現是向nextResponder
轉發方法。
當觸摸事件在響應鏈上傳遞時,判斷當前UIResponder
能否響應的條件是:其是否實現了touchesBegan
方法。
在這些UITouches
序列的生命周期方法中,我們可以獲取對應UIEvent
與UITouch
,利用它們所提供的信息,進一步決定如何響應用戶的觸摸事件。