這篇是總結(jié)上一周在開發(fā)中遇到的問題,以及針對這個問題又進(jìn)一步做了研究后得出的結(jié)論和經(jīng)驗。一來是幫助自己深入理解UIKit的工作機(jī)制,形成自己的體系風(fēng)格;二來是盡量幫助大家避過我踩過的坑.
拋出問題
目前我正在做照片編輯器的軟件(photo collage), 簡單來說就是讓用戶編輯,組合自己的照片,然后上傳到walgreens的服務(wù)器進(jìn)行打印,之后用戶去店里去取。和國內(nèi)美圖秀秀這類軟件還是有一些區(qū)別。既然是照片編輯,那么旋轉(zhuǎn),縮放,平移這些對相片基本的操作就必不可少,但也是這些看上去不起眼的功能往往會給你帶來不小的困擾. 問題背景如下:
如圖所示,這是App的 "Editing View" 我只把中間的主要部分重畫了下,用來闡述問題。灰色部分是最外層的container view, 姑且叫它 “root container view”(此環(huán)境下); A, B, C各自代表一個container view,并且在每個container view中,有一個imageView,我們可以對這個imageView進(jìn)行一些基本操作,旋轉(zhuǎn),平移,縮放等。所以,在imageView上,就被添加了3個gesture recognizers。OK, 問題到目前為止還是很清晰的,好,我們接著往下走。在A, B, C這3個container上,也加了一個gesture, 叫“l(fā)ongpress gesture”用來處理用戶長按操作,這個操作是用作交換A,B,C三個container view位置的。當(dāng)用戶長按某個container view,觸發(fā)了longPressure操作后就可以進(jìn)行拖拽以交換位置。
啰嗦了這么多把背景講完,下面說下問題。老板突然要求要加入3D force touch,以便outstanding。本身加入force touch是一個很簡單的工作(quick actions, peek and pop, 以及新屬性應(yīng)用), 但是背靠萬惡的“l(fā)egacy code”,添加新功能引入bug是不可避免的了。老板的要求是使用3D force touch觸發(fā)之前l(fā)ongpress的操作,聽上去很簡單,只是把觸發(fā)操作改了嘛其他的不動就好了,最多extract幾個函數(shù)出來嘛,此時我的心情是:
開始解決問題
這里講述我解決問題時的心路歷程,如果您著急解決問題可直接跳過這部分,到第三部分。
OK, 那就讓我們來看下怎么觸發(fā)3D force touch。 在iOS9中, Apple為UITouch增加了2個新的屬性: force和maximumPossibleForce。force代表的就是你的按壓力度,通常的默認(rèn)值是1,由系統(tǒng)決定,被當(dāng)做一個average touch。其真實值是0~6.66666...., 很多6....., 有些國外的網(wǎng)友測試過,6.66666....就是385克,這也是**maximumPossibleForce****的值. Apple提供范圍如此之大的上下限就是讓開發(fā)者可以積極利用force這個屬性,開發(fā)出精準(zhǔn)交互的App。
Ok,那我們在哪里調(diào)用這個touch對象呢?毫無疑問,我們首先想到的就是Respond Chain API, 那一系列的touchXXX方法。force touch是一個持續(xù)的過程,所以我們并不在touchBegan:withEvent中做force的檢測,而是在touchMoved:withEvent中判斷force的變化是否達(dá)到我們所設(shè)置的閾值. 當(dāng)然,UIResponder中還有一系列pressXXX方法,例如pressesBegan:withEvent, 但這些是給physical button點擊時使用的,例如Apple TV的遙控器等,對于普通的surface touch,還是touchXXX方法響應(yīng)。
這時候,問題來了,在響應(yīng)鏈API中,我們獲取到的touch對象上的view是最底層的那個view,也就是說,當(dāng)我們長按container view觸發(fā)移動操作時,我們拿到的touch.view實際上是最下面的imageView, 而不是對應(yīng)的container view。這會帶來什么問題呢?因為image view本身就綁定了3個gesture,這些gesture本身就能響應(yīng)用戶的操作,而現(xiàn)在新加入的touchXXX系列方法也同樣響應(yīng)UITouch事件,處理不好就會有沖突,甚至崩潰,這也是為什么我本篇題目定位為混合編程的原因。
我首先想到的解決辦法就是將所選imageView對應(yīng)的container view找出來,之后再container view上加效果,做移動。但馬上發(fā)現(xiàn)其實這樣做是不可取的,因為touch.view返回值就是imageView,這個在整個touch事件周期中是不會改變的,而且在touchXXX API中,你能移動的必須是,也只能是一開始選中的imageView。那好,那我們可以做個測試,我把imageView提取了出來,加到了root container中,來做移動,這時候,imageView的確可以自由移動了,但是當(dāng)你釋放手指的時候,touchEnded:withEvent方法并沒有被調(diào)用, 導(dǎo)致結(jié)束函數(shù)無法被觸發(fā)。具體的原因我猜測是因為imageView被我強(qiáng)行加入到了頂層container,破壞了hitTest鏈,也就是響應(yīng)鏈信息被破壞,導(dǎo)致touchEnded:withEvent無法被調(diào)用,沒有對象觸發(fā)它。當(dāng)我們移走imageView,再在其對應(yīng)的空白container view上點擊下的話,touchEnded:withEvent會被調(diào)用。
這時候,我的心情....
真正解決問題
下了班和朋友討論了下,他提供了一個折衷的方案(又印證了經(jīng)驗豐富的老司機(jī)感覺不會錯的觀點---我說的): 使用touchMoved:withEvent來檢測force touch,觸發(fā)操作; 之后使用UIPangestureRecogonizer對象進(jìn)行之后的所有操作. 我聽完后整個人就開朗了~~~~
第二天,我要把這個theory付諸實際,開始調(diào)試代碼,在touchMoved:withEvent:加入force touch的觸發(fā)判斷,之后做一個標(biāo)記(boolean值)記錄force touch已被觸發(fā)。OK, 開始運行程序,長按后,的確觸發(fā)了force touch但是當(dāng)我拖拽的時候,對應(yīng)的container view并沒有移動,這就奇怪了,pangesture recogonizer已經(jīng)加到了每個container view上面啊,為什么不觸發(fā)呢?思前想后,八成又是響應(yīng)鏈API和手勢識別混用的后果,但Apple應(yīng)該也給出了控制它們響應(yīng)的方法。所以我打開了UIGestureRecognizer的頭文件,發(fā)現(xiàn)在UIGestureRecognizerDelegate中有一個方法:gestureRecognizerShouldBegin,該方法控制著gesture是否該響應(yīng)的返回值, 而我之前的拖拽無響應(yīng)是不是和這個delegate method有關(guān)呢?我在當(dāng)前的view contrller實現(xiàn)了這個方法,果不其然,當(dāng)返回為YES時,panGesture順利工作了!但是,container view里面的imageView也跟著滑動,這個是不需要的。所以,當(dāng)觸發(fā)force touch時,讓imageView的gestureRecognizerShouldBegin:返回空即可(可以暴露imageView一個public attr給外部類,當(dāng)觸發(fā)force touch時, 將這個attr賦值為YES, 至于gestureRecognizerShouldBegin:, 系統(tǒng)在每次用戶滑動時都會觸發(fā),所以你只需在該函數(shù)開頭加入正確的判斷即可,即手勢類型判斷和標(biāo)記為判斷)。
OK,既然說到了gestureRecognizerShouldBegin:我們就再深入研究下它的調(diào)用。該方法的調(diào)用可不是在實現(xiàn)該方法的類里面調(diào)用一次那么簡單。這樣說可能還有些晦澀,我們來舉個栗子:
圖中有3中顏色, 分別代表3個不同大小的view,小的view是大的view的subview。在每個view上,我都加上了tap gesture recognizer,并且在每個類里面,都實現(xiàn)了gestureRecognizerShouldBegin:. 運行程序后,我們在最內(nèi)層的藍(lán)色view上點擊下,程序立刻在藍(lán)色view的gestureRecognizerShouldBegin:停下(打了斷點), 但這時候,tap gesture所持有的view卻不是藍(lán)色的view,而是橙色和外層綠色或者藍(lán)色中的某一個(隨機(jī)的,我測試過10次以上了...); OK, 為了之后方便講解,我們假定tap gesture第一次點擊持有的view就是藍(lán)色view,當(dāng)我們點擊繼續(xù)后(聲明: 3個view的gestureRecognizerShouldBegin:均返回YES),橙色view的gestureRecognizerShouldBegin:被調(diào)用了,點擊繼續(xù)后,又跳回藍(lán)色view的gestureRecognizerShouldBegin:方法中,再點擊繼續(xù),則跳到綠色view的gestureRecognizerShouldBegin:方法。綜上來說,最底層的subview的gestureRecognizerShouldBegin:方法被調(diào)用了3次, 有且只有最底層的view的gestureRecognizerShouldBegin:返回為YES時,上層的gestureRecognizerShouldBegin:才會被調(diào)用。這種機(jī)制會帶來什么后果或者說bug呢? 如果你在最底層的subview中的gestureRecognizerShouldBegin:方法判斷錯誤的話, 一旦其放回NO, 上層的gesture都不會被觸發(fā),手勢識別全部不會響應(yīng).聽上去好嚴(yán)重的樣子, 那有沒其他方法可以代替呢?有的,那就是shouldReceiveTouch:這個方法, 這個方法會在touchBegan:withEvent: 之前調(diào)用, 目的在于告訴gesture recogonizer是否應(yīng)該接收touch事件,如果返回NO,則不接收。這個方法在每個view只會調(diào)用一次,不存在底層subview調(diào)用多次的情況。
UIGestureRecognizerDelegate還有其他的函數(shù),但都比較簡單,使用場景比較單一大家可以自行看下.
好,回到正題。解決了觸發(fā)pan gesture的問題之后,新的問題來了。當(dāng)用戶僅觸發(fā)了force touch并不移動時, 系統(tǒng)不會發(fā)送touchEnded:withEvent:回來, 導(dǎo)致收尾方法無法被調(diào)用,造成用戶體驗上的缺失.那原因是什么的?既然是gesture recogonizer出了問題,那就去它的頭文件看看。我發(fā)現(xiàn)了2個比較有意思的屬性: delaysTouchesBegan和delaysTouchesEnded。delaysTouchesBegan是用來告訴響應(yīng)鏈你的touchBegan:withEvent:別調(diào)用,一切讓我的gesture識別器來此處理,這個屬性默認(rèn)值是NO; delaysTouchesEnded這個屬性默認(rèn)值是YES,告訴響應(yīng)鏈你的touchEnded:withEvent:別管,我gesture識別器來做。這個屬性就是造成之前提到bug的罪魁禍?zhǔn)? 在往view上天劍gesture時,將此屬性置為NO即可。當(dāng)然那你也不用擔(dān)心,當(dāng)觸發(fā)pan gesture時,結(jié)束函數(shù)就是gesture控制的,不會在調(diào)用touchEnded:withEvent:方法了。
總結(jié)
好了,到此為止,問題解決了。讓我對響應(yīng)鏈和gesture識別器又多了一層的了解。