這篇文章是我在閱讀相關(guān)蘋(píng)果官方文檔后總結(jié)整理出來(lái)的一些平持馗叮可能不太注意到,但是又比較有用的知識(shí)點(diǎn)凫乖。如有錯(cuò)誤确垫,歡迎指出。
事件傳遞
事件本質(zhì)
什么是事件帽芽?官方文檔的解釋是:
Events in iOS represent fingers touching views of an application or the user shaking the device. One or more fingers touch down on one or more views, perhaps move around, and then lift from the view or views. As this is happening, iPhone’s Multi-Touch system registers these touches as events and sends them to the currently active application for processing
當(dāng)觸摸事件發(fā)生時(shí)删掀,系統(tǒng)會(huì)把觸摸注冊(cè)為一個(gè)事件(Event),傳遞給系統(tǒng)處理导街。一個(gè)完整的手勢(shì)過(guò)程披泪,是從第一根手指觸碰到屏幕開(kāi)始,到最后一根手指離開(kāi)屏幕為止搬瑰。
當(dāng)然款票,手機(jī)的搖晃也要算是Event(它屬于UIEventType里的motion),不過(guò)那是另一回事了泽论,我們后邊會(huì)再說(shuō)艾少。
屏幕上的每一個(gè)觸摸點(diǎn)用UITouch來(lái)表示。在整個(gè)手勢(shì)過(guò)程中翼悴,每一個(gè)UITouch對(duì)象會(huì)被系統(tǒng)持有缚够,但是它的狀態(tài)是可變的,分別要經(jīng)歷touchesBegan
, touchesMoved
和 touchesEnded
三個(gè)狀態(tài)抄瓦。
當(dāng)然潮瓶,個(gè)別的時(shí)候,一個(gè)UITouch會(huì)經(jīng)歷第四個(gè)狀態(tài):touchesCanceled钙姊。一個(gè)事件被取消通常是由于一個(gè)外部事件(例如來(lái)電)的產(chǎn)生毯辅,讓系統(tǒng)終止了本次touch事件。
UITouch
每一個(gè)UITouch對(duì)象表示一根手指對(duì)屏幕的觸摸煞额,包含位置思恐、大小、移動(dòng)狀況以及觸摸的力度(力度僅在支持3Dtouch或者Apple Pencil的設(shè)備上管用)膊毁。
UITouch類(lèi)有以下我覺(jué)得比較重要的屬性和方法:
- locationInView: 表示觸摸點(diǎn)在給定視圖對(duì)應(yīng)坐標(biāo)系下的位置胀莹。如果view參數(shù)傳nil,那么給出的是touch在window對(duì)于坐標(biāo)系中的位置婚温。
- previousLocation(in:) 表示前一次該touch在給定視圖中的方位描焰。
- view 表示touch對(duì)象被“傳送”到的view。注意,這個(gè)view并不一定是touch對(duì)象本身所在的view(即荆秦,不一定是用戶(hù)手指點(diǎn)中的view)篱竭。例如,當(dāng)一個(gè)gestureRecognizer接收到一個(gè)觸摸事件時(shí)步绸,view為nil掺逼,因?yàn)闆](méi)有view在接收這個(gè)觸摸。
- preciseLocation(in:) 這個(gè)表示一個(gè)touch在給定視圖中的精確方位瓤介。注意吕喘,不要把返回的CGPoint用于hitTest。有的時(shí)候hitTest返回值顯示touch在給定view中刑桑,但是preciseLocation方法返回的值卻表明touch不在view中氯质。
- phase:表示UITouch對(duì)象的幾個(gè)階段。按照順序依次變化:began, moved, stationary, ended/canceled
響應(yīng)者鏈
UIResponder
UIResponder是一個(gè)抽象類(lèi)漾月,被蘋(píng)果稱(chēng)為事件處理的“主心骨”病梢。具體到事件發(fā)生時(shí),繼承自UIResponder的對(duì)象主要有兩個(gè)方面的職責(zé):
- 通過(guò)覆寫(xiě)四個(gè)關(guān)于touches的方法梁肿,攔截并處理事件(如果該對(duì)象需要響應(yīng)事件的話)蜓陌。
- 將事件順著響應(yīng)者鏈向上傳遞(如果該對(duì)象不需要響應(yīng)該事件的話)。
另外吩蔑,inputView也可以作為事件的響應(yīng)(在這里我把它理解為“輸入響應(yīng)”)钮热。例如,當(dāng)我們點(diǎn)擊一個(gè)textView烛芬,這個(gè)view會(huì)變成First Responder隧期,并顯示它的 inputView。關(guān)于inputView和firstResponder我們會(huì)另起一篇文章來(lái)詳細(xì)描述它赘娄。
接下來(lái)我們看幾個(gè)UIResponder當(dāng)中重要的屬性和方法:
nextResponder
顧名思義仆潮,它表示響應(yīng)者鏈中的下一個(gè)響應(yīng)者。值得注意的是遣臼,UIResponder本身并不存儲(chǔ)或者預(yù)先設(shè)置任何值給nextResponder性置,該屬性默認(rèn)設(shè)置為nil。到底誰(shuí)是nextResponder還需要繼承自它的類(lèi)自己來(lái)覆寫(xiě)揍堰。例如鹏浅,一個(gè)View的nextResponder可能是它的superView(如果有的話),也可能是viewController(如果該view就是根視圖)屏歹。一個(gè)ViewController的nextResponder可能是UIWindow(如果其根視圖是這個(gè)window的root view的話)隐砸,也可能是另一個(gè)viewControllerB(如果viewController嵌套在viewControllerB中顯示的話)。UIWindow的nextResponder就是UIApplication蝙眶。UIAPPlication的nextResponder就是appDelegate(當(dāng)且僅當(dāng)這個(gè)delegate是UIResponder的實(shí)例而非一個(gè)view季希,viewController,或者app object本身)。
isFirstResponder
字面意思胖眷,“是否是第一響應(yīng)者”武通。關(guān)于這個(gè)第一響應(yīng)者,目前我暫時(shí)無(wú)法獲得一個(gè)準(zhǔn)確的定義珊搀,但基本可以肯定的是,此處的第一響應(yīng)者和事件傳遞過(guò)程中尋找的“最合適的響應(yīng)者”并非同一回事尾菇。因此境析,這部分暫時(shí)略過(guò),等找到準(zhǔn)確定義之后再發(fā)文說(shuō)明派诬。canBecomeFirstResponder
字面意思劳淆,表示一個(gè)對(duì)象是否能夠成為第一響應(yīng)者。UIKit會(huì)把某些事件默赂,例如motion event沛鸵,分發(fā)給“第一響應(yīng)者”。默認(rèn)返回No缆八。becomeFirstResponder
讓消息接收者成為第一響應(yīng)者曲掰。這個(gè)方法相信咱們?cè)陂_(kāi)發(fā)過(guò)程中都快要寫(xiě)爛了——遇到textField,調(diào)用此方法奈辰,讓系統(tǒng)彈出鍵盤(pán)栏妖。文檔指出,對(duì)某個(gè)對(duì)象調(diào)用該方法后奖恰,并不能保證該對(duì)象一定能夠成為firstResponder吊趾,因?yàn)椋琔IKit會(huì)首先對(duì)當(dāng)前的firstResponder發(fā)送resignFirstResponder消息瑟啃,然而后者可能會(huì)失斅鄯骸(例如自定義的對(duì)象重寫(xiě)了resignFirstResponder,通過(guò)return NO
拒絕退出第一響應(yīng)者狀態(tài))蛹屿。
如果當(dāng)前firstResponder成功地resign了屁奏,UIKit還要調(diào)用當(dāng)前對(duì)象的canBecomeFirstResponder
方法,而如上文所言蜡峰,后者默認(rèn)返回NO了袁。
再如果,canBecomeFirstResponder
返回了YES——那么該對(duì)象將成為第一響應(yīng)者湿颅。至此载绿,所有發(fā)送給第一響應(yīng)者的事件都被指派給這個(gè)對(duì)象,且系統(tǒng)將會(huì)試圖展示該對(duì)象的inputView油航。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *) event
默認(rèn)情況下崭庸,不管你用了幾根手指去點(diǎn)這個(gè)view,touches集合中僅包含一個(gè)UITouch 對(duì)象。如果你希望接收到多個(gè)手指的觸控怕享,記得調(diào)用view.isMultipleTouchEnabled=true执赡。
有兩個(gè)注意點(diǎn):
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *) event
- 該方法的默認(rèn)實(shí)現(xiàn)是將事件沿響應(yīng)者鏈向上傳遞。因此函筋,如果你想要覆寫(xiě)該方法沙合,要確保調(diào)用了super的touchesBegan方法以傳遞任何你自身不處理的事件!
- 如果你覆寫(xiě)該方法的時(shí)候沒(méi)有調(diào)用super跌帐,那么你需要在你的自定義類(lèi)中同時(shí)調(diào)用其他touches相關(guān)的方法首懈,哪怕在這些方法中什么都不做。
事件攔截
UIEvent
- 它用于表示用戶(hù)和APP的一個(gè)交互的對(duì)象谨敛。
- 永遠(yuǎn)不要retain一個(gè)UIEvent對(duì)象究履,或者是其內(nèi)部的屬性。如果的確需要retain某個(gè)UIEvent自帶的屬性脸狸,應(yīng)對(duì)后者使用copy操作最仑。
- 包含四個(gè)type: touches, motion, remote-Control, presses。motion是由UIKit觸發(fā)的(要和Core Motion Framework的motion event區(qū)分開(kāi)來(lái)炊甲。)remote-Control指的是用戶(hù)通過(guò)外部配件(比如耳機(jī)泥彤、遙控器)對(duì)設(shè)備發(fā)出的操作指令。Press事件指的是用戶(hù)通過(guò)游戲控制器蜜葱、遙控器等的實(shí)體按鍵來(lái)和設(shè)備進(jìn)行的交互行為全景。所有的這些可以通過(guò)UIEvent的type和subtype屬性來(lái)加以判斷。
接下來(lái)介紹一些重要的屬性和方法:
func touches(for view: UIView) -> Set<UITouch>?
返回該事件中牵囤,屬于指定view上的所有touch爸黄。
func touches(for window: UIWindow) -> Set<UITouch>?
和上面類(lèi)似。
func touches(for gesture: UIGestureRecognizer) -> Set<UITouch>?
返回該手勢(shì)識(shí)別器所接收到的所有UITouch對(duì)象揭鳞。
func coalescedTouches(for touch: UITouch) -> [UITouch]?
這個(gè)方法是在iOS9之后提出的炕贵,它利用了一種叫做“觸摸合并”的技術(shù)。由于系統(tǒng)對(duì)touch的采樣在touchesMoved方法中進(jìn)行野崇,而后者的調(diào)用頻率最高也才60次/秒(如果主線程有其他高耗時(shí)的操作称开,該方法的調(diào)用頻率甚至更低),這樣乓梨,就不可避免地會(huì)出現(xiàn)“漏點(diǎn)”的情況鳖轰。而在新的iPad Pro2代上,界面刷新率達(dá)到了120Hz(使用Apple pencil時(shí)刷新率一度飆升至200Hz)扶镀,因此蕴侣,使用傳統(tǒng)的touchesMoved必然會(huì)造成一個(gè)奇觀:用戶(hù)的手指在前面劃線片酝,畫(huà)出來(lái)的線在后邊追趕用戶(hù)的手指……抑或是用戶(hù)命名畫(huà)了一條弧線呻引,得到的卻是一條“折線”……
基于此,蘋(píng)果提出了觸摸擬合技術(shù)虐急,它可以讓你獲取到所有在兩次touchesMoved調(diào)用之間的UITouch對(duì)象。
使用方法如下:
''override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)"
''{
'' if let coalescedTouches = event.coalescedTouchesForTouch(touch) {
'' print("coalescedTouches:", coalescedTouches.count)
''
'' for coalescedTouch in coalescedTouches
'' {
'' //Additional operations
'' }
''
'' }
'' }
`func predictedTouches(for touch: UITouch) -> [UITouch]?
同樣是在iOS9以后狞膘,同樣是為了減少延遲揩懒,蘋(píng)果還推出了觸摸預(yù)測(cè)技術(shù),它根據(jù)先前觸摸的點(diǎn)挽封,使用一套非常精密的算法已球,來(lái)大致預(yù)測(cè)下一個(gè)被觸摸的點(diǎn)所在的坐標(biāo)。因此场仲,開(kāi)發(fā)者可以使用預(yù)測(cè)出來(lái)的點(diǎn)來(lái)提前做好UI更新的準(zhǔn)備和悦。
使用方法和func coalescedTouches(for touch: UITouch) -> [UITouch]?
基本一樣,在此不多贅述渠缕。
UITouch,UIEvent褒繁,UIResponder亦鳞,UIGestureRecognizer的區(qū)別與聯(lián)系
這一部分就算作是本文的小結(jié)了。我們來(lái)梳理一下四者的區(qū)別和聯(lián)系:
- UITouch:表示一根手指在屏幕上的觸摸棒坏、移動(dòng)燕差,其生命周期從手指觸摸屏幕時(shí)開(kāi)始,到手指離開(kāi)屏幕(或者被cancel)為止坝冕。
- UIGestureRecognizer: 手勢(shì)識(shí)別器徒探。一個(gè)手勢(shì)可能要一根or多根手指來(lái)完成,因此一個(gè)手勢(shì)包含多個(gè)UITouch對(duì)象喂窟。
- UIEvent:表示“事件”测暗,有三個(gè)大類(lèi):觸摸事件、動(dòng)作事件磨澡、遠(yuǎn)程事件碗啄。一個(gè)觸摸事件(touch event)包含了一個(gè)或多個(gè)與該事件有關(guān)的觸摸對(duì)象,后者用UITouch對(duì)象來(lái)表示稳摄。
- UIResponder:響應(yīng)事件的一個(gè)“抽象類(lèi)”稚字,需要響應(yīng)事件的類(lèi)必須繼承自它。多個(gè)響應(yīng)者組成響應(yīng)者鏈厦酬。
另外胆描,一個(gè)完整的觸摸序列,是從第一根手指按下開(kāi)始仗阅,到最后一根手指一開(kāi)屏幕為止昌讲。