喬幫主在發(fā)布會上提到盏求,用戶的手才是最好的輸入設備抖锥,的確亿眠,iPhone之后,非觸屏手機再已難覓磅废。觸摸是最基本的用戶輸入事件缕探,理解iOS特有的觸摸事件響應機制,能夠良好管理程序中觸摸響應方法还蹲,避免沖突的發(fā)生爹耗。
iOS中的事件
iOS中的事件主要分為三類:
- UIControl Actions: 使用target/action注冊的SEL。
- User Events: 用戶與應用之間的交互:觸摸谜喊,輸入文字潭兽,搖晃,遠程控制等斗遏。
- System Events: 應用啟動山卦,切前后臺,低內(nèi)存等诵次。
cocoa和cocoa touch的程序啟動后账蓉,,會首先初始化一些基本資源:在主線程創(chuàng)建一個main event loop逾一;初始化主UIWindow
铸本。
應用啟動過程-w500
main event loop本質(zhì)上是一個NSRunLoop
,與其他輔助線程的run loop不同遵堵,其是自創(chuàng)建后自動開始運行的箱玷。主消息循環(huán)最大的特點是:它在創(chuàng)建時就與負責捕獲用戶事件的系統(tǒng)底層建立了連接,所以它的input source可以收到系統(tǒng)傳遞過來的用戶事件陌宿。UIApplication
對象會將當前要處理的用戶事件封裝成UIEvent
,發(fā)送給UIWindow
,在由UIWindow
轉(zhuǎn)發(fā)給對應的響應者锡足。
iOS響應用戶事件
UIEvent
表示用戶與iOS產(chǎn)生交互的事件,UIWindow
將觸摸事件發(fā)送給hitTest View,其他事件發(fā)送給first responder壳坪,若它們不能處理該事件舶得,事件在響應鏈向上傳遞,找到最終的響應者或丟棄爽蝴。
本文主要介紹觸摸事件的響應機制沐批。
iOS中能夠捕獲觸摸事件的類
iOS程序中,有三種類可以接受用戶的觸摸事件并響應霜瘪,分別是:UIControl
, UIReponder
, UIGestureRecognizer
珠插,這三個類在參與觸摸響應機制的時機不同,在實際使用時要加以注意颖对。
iOS中的觸摸事件
iOS中使用UItouch
來表示用戶的一根手指在屏幕上的觸摸行為捻撑。當用戶觸摸屏幕時,硬件會捕捉到觸摸行為,將觸摸點的半徑顾患、力度和坐標等發(fā)送給iOS番捂,經(jīng)過UIKit
封裝后,得到UITouch
對象江解。通過UITouch
對象设预,我們可以獲得其關(guān)聯(lián)的視圖(hitTest View),在視圖中的坐標,生命周期的當前階段犁河,點擊數(shù)等信息鳖枕。。一次用戶點擊多次的事件桨螺,其只包含一個UITouch
觸摸類型的UIEvent
包含至少一個UITouch
宾符,也就是用戶在屏幕上的一次手勢操作的手指運動,其會持有此次事件相關(guān)聯(lián)的UITouches
序列灭翔。魏烫,即在一次手勢操作中,其中一個手指中途離開屏幕肝箱,它所對應的UITouch
依然存在于該事件中哄褒。響應者會在touchesBegan:withEvent:
等方法中獲取UITouch
對應的UIEvent
。
UITouches
序列在用戶第一根手指觸摸屏幕時開始煌张,最后一根手指離開時結(jié)束呐赡,當手指狀態(tài)變化時,iOS會將序列中的UITouch
對象發(fā)送給UIEvent
對象唱矛。

iOS的觸摸事件響應機制
當用戶觸摸屏幕時罚舱,對應的觸摸事件會加入到UIApplication
事件隊列中井辜,當下一個RunLoop來臨時绎谦,UIApplication
會將出列最前端的事件,發(fā)送給當前的UIWindow
(key window
)粥脚。
UIWindow
會調(diào)用hitTest:withEvent:
方法窃肠,開始hit-testing流程尋找包含觸摸點的視圖。該流程會返回包含觸摸點的層級最低的視圖刷允。
每當用戶觸摸屏幕時冤留,UIKit
都會執(zhí)行hit-testing,之后再從hitTest視圖開始尋找事件的響應者树灶。當hitTest視圖決定后纤怒,它就關(guān)聯(lián)了對應的觸摸事件,會持續(xù)收到觸摸事件生命周期的方法,(touchBegan, touchMove, touchCancel/touchEnd),即使是觸摸點已經(jīng)在touchMove階段移出了hitTest視圖天通,它依然能夠收到后續(xù)的消息泊窘。
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視圖是觸摸事件的響應鏈頭結(jié)點。
逆前序的深度遍歷算法:根節(jié)點-->右子樹-->左子樹烘豹。
當收到觸摸事件后,UIApplication
在當前視圖層級中,從key window
開始(最頂級)揣钦,從上往下遍歷子視圖調(diào)用hitTest:withEvent:
移层,若找到hitTest視圖則停止遍歷并返回。
當視圖收到hitTest:withEvent:
方法后憔鬼,通過下列條件判斷是否在該視圖執(zhí)行hit-testing龟劲。
-
pointInside:withEvent:
方法返回YES。pointInside:withEvent:
方法用來判斷觸摸點是否在當前視圖內(nèi)轴或。 - hidden == NO咸灿。
- userInteractionEnabled == YES。
- alpha >= 0.01侮叮。若view的content繪制為透明的避矢,則不受影響。
需要注意的是囊榜,當clipsToBounds == NO時审胸,視圖的子視圖可能會超出其bounds,這種情況如果觸摸點在子視圖超出父視圖的范圍卸勺,那么hit-tesing不會再此視圖樹上執(zhí)行砂沛。
如圖,當用戶觸摸
viewB.1
時曙求,UIApplication
對象收到觸摸事件碍庵,從key window
開始執(zhí)行hit-testing,首先訪問viewC
悟狱,由于pointInside:withEvent:
方法返回NO静浴,取消執(zhí)行并訪問viewB
,滿足執(zhí)行挤渐,則從右往左開始訪問其子視圖(視圖層級從下往上)苹享,找到viewB.1
,它沒有子視圖浴麻,則返回自己得问。最終UIWindow
對象將viewB.1
作為hitTest視圖返回給UIApplication
對象。可以看到软免,當某一視圖收到
hitTest:withEvent:
方法后宫纬,它會向所有子視圖發(fā)送hitTest:withEvent:
方法,若它的沒有子視圖或所有子視圖返回nil膏萧,那么就返回自己漓骚,所有hit-testing流程最終一定會找到一個對象UIView/UIWindow
去接收觸摸事件宣蔚。以下是
hitTest:withEvent:
可能的實現(xiàn)。
- (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
對象組成的鏈形結(jié)構(gòu)认境,它以first responder為頭結(jié)點胚委,UIApplication
對象為尾節(jié)點,事件從頭開始在響應鏈中向上傳遞叉信。
UIResponder
用來設計處理事件亩冬,UIApplication
, UIViewController
, UIView
都是其子類,只要它們實現(xiàn)了UIResponder
中的鉤子方法硼身,就可以響應對應的事件硅急。

其中
first responder
用來第一個接觸事件,可以使用becomeFirstResponder
來設置它佳遂,主要要在視圖層級已經(jīng)完全建立之后再設置营袜。
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
開始的響應鏈處理,其他的動作事件迹卢,遠程事件辽故,系統(tǒng)事件等,會交給first responder
開始的響應鏈處理婶希。
UIKit會將用戶事件發(fā)送給理論上最合適的對象榕暇。所以當程序中的響應者要經(jīng)過很長的查找路徑時,這時就要考慮是否實現(xiàn)是否設計合理了喻杈。
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開頭的響應鏈尋找能夠最終的響應者。

當找到響應者或已經(jīng)到鏈尾(UIApplication)仍不能處理壁晒,UIKit會停止查找瓷们,對于后者,對應的事件會被丟棄。
除了UIResponder
對象谬晕,UIGestureRecognizer
與UIControl
也可以響應觸摸事件碘裕,但它們參與觸摸事件響應的方式不同。
-
UIGestureRecognizer
在響應鏈中的位置取決于依附的視圖攒钳。 -
UIControl
參與響應的方式?jīng)Q定于其關(guān)聯(lián)的target帮孔。
UIGestureRecognizer
要先于視圖收到觸摸事件,但需要注意的是不撑,若該視圖也可以響應觸摸事件(實現(xiàn)了UITouch
生命周期函數(shù))文兢,那么手勢對象并不會阻礙視圖的響應,雙方是同時響應的焕檬,只不過存在先后順序姆坚。
UIGestureRecognizer與UIView的接觸事件的次序
響應觸摸事件
當確定了響應鏈后,UIWindow
會向hitTest View
發(fā)送以下方法:
- (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
用于響應觸摸事件的方法实愚,這些鉤子方法的默認實現(xiàn)是向nextResponder
轉(zhuǎn)發(fā)方法兼呵。
當觸摸事件在響應鏈上傳遞時,判斷當前UIResponder
能否響應的條件是:其是否實現(xiàn)了touchesBegan
方法腊敲。
在這些UITouches
序列的生命周期方法中萍程,我們可以獲取對應UIEvent
與UITouch
,利用它們所提供的信息兔仰,進一步?jīng)Q定如何響應用戶的觸摸事件茫负。