1. 程式人生 > >IOS手勢處理的那些坑

IOS手勢處理的那些坑

前言

在不考慮內部實現機制的情況下,我們使用三種方式來處理IOS手勢:
1. Gesture Recongnizers — UIGestureRecognizer 及其子類
2. touches 響應 — touchesBegan、touchesEnd..等
3. Target-Action 機制 — UIControl及其子類

本文探討了這幾種處理手勢事件的混合使用可能會產生的衝突情況,並提供瞭解決方法,希望看過這篇文章的朋友們不再踩這些坑。

想象這樣一個場景,我們自定義了一個View,使用UIView作為其父類,通常我們只是將其本身作為一個承載子view的容器,但是很多情況下我們不得不對其進行拓展使其能夠響應使用者手勢(例如點選跳轉),有3個方案可選。
1. 繫結UIGesture Recognizer ,Apple推薦做法


2. 實現UIVIew的touch事件touchesEnded:withEvent
* 如果父檢視響應touchesBegan:withEvent 事件的話,見第二節 Touch Events 衝突
* 如果父檢視響應UIGesture Recongnizer 事件的話,見第三節 Touch Events 衝突
3. 將父類從UIView改成UIControl
* 如果父檢視響應UIGesture Recongnizer ,見第四節 UIGestureRecognizer和UIControl的衝突

Touch Events 衝突

在這個例子中,我們讓父view響應touchesBegan事件,子view(背景紅色,佔據父view的上半部分)的touchesEnded處理跳轉事件
* 當點選下半部分割槽域時,父view背景改變
* 當點選上半部分割槽域時,彈出跳轉提示(期望結果),同時,父view背景改變(非期望結果)

問題分析
  • 當點選下半區域時,Hitest尋找到響應控制元件父view,touchesBegan更改了背景顏色
  • 當點選下半區域時,在touchesBegan階段,hitest尋找響應控制元件子view,但是由於子view無法響應touchesBegan,所以通過響應鏈尋找到父view處理此touch事件,父view背景因此更改。
    同理,在touchesEnded階段,Hitest尋找到了響應控制元件子view,所以彈出了跳轉提示。
解決方案

除了使用UIGestureRecognizer代替touchesEnded之外,還可以通過實現子view所有的touches事件來防止touches event 被響應鏈傳遞。

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    防止此事件被響應鏈傳遞
}

UIGestureRecognizer和touches事件的衝突

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer.

為了搞清楚這兩者同時存在時響應的時序問題,我們在view上繫結tapGestureRecognizer的同時實現所有的touches事件,嘗試以下操作並觀察結果

  1. 點選
2016-10-30 15:37:47.711 LOG:::VCTouchesVSGestures___view:touchesBegan:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
2016-10-30 15:37:47.716 VCTouchesVSGestures: 0x7f9183ea8510>
2016-10-30 15:37:47.717 LOG:::VCTouchesVSGestures___view:touchesCancelled:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
  1. 拖動
2016-10-30 15:44:16.222 LOG:::VCTouchesVSGestures___view:touchesBegan:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
2016-10-30 15:44:16.236 LOG:::VCTouchesVSGestures___view:touchesMoved:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
2016-10-30 15:44:16.267 LOG:::VCTouchesVSGestures___view:touchesMoved:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
....
....
2016-10-30 15:44:18.137 LOG:::VCTouchesVSGestures___view:touchesEnded:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
問題分析
  1. 我們發現,在手勢事件剛觸發時,view便接到了touchesBegan事件,同時隨著tap gesture被識別出,系統向view傳送了touchesCancelled事件,於是view的touches響應就此結束
  2. 如果我們的手勢被解析失敗,那麼view的touches事件將會持續觸發,直到截止
解決方案

UIGestureRecognizer類提供了三個屬性來處理這兩者之間的關係,它們分別是:

  • cancelsTouchesInView

    預設為YES,當手勢識別成功時,UIApplication將傳送touchesCancel訊息。

    設定為NO時,即使手勢識別成功,UIView仍然會正常響應touches事件

  • delaysTouchesBegan

    預設為NO,如果為YES,只要gestureRecognizer沒有識別失敗(識別中or識別成功),那麼view永遠接受不到touchesbegan。在識別失敗的情況下,gesture繫結的view將會被觸發touchesBegan(可能跟著一系列的touchesMove)

  • delaysTouchesEnded

    預設為YES,其效果類似delaysTouchesBegan

有興趣的讀者可以將上述例子中的gesture的上述三個屬性修改一下,檢視效果

UIGestureRecognizer和UIControl的衝突

點選程式的任意區域,都沒有跳轉提示,只有父view的背景不斷變化,似乎子view新增的手勢處理事件完全沒有被觸發。

問題分析

經過除錯發現,完全沒有響應子view的UIControl的touchUpInside事件,而是響應了父控制元件的tap事件。是因為UIGesture Recognizer的級別較高,而且由於cancelsTouchesInView預設是yes,所以UIControl不足以識別出touchUpInside事件,假如我們將UIControl的響應事件改成touchDown的話,你會發現,兩者都響應。同樣,保留touchUpInside將cancelsTouchesInView設定成NO,有同樣的效果。
實際上,UIGestureRecognizer和UIControl的衝突本質上是UIGestureRecognizer和Touches響應的衝突,可參考上一節。
但是,並不是UIControl的所有子類都會遭遇同樣的問題。假如我們把UIControl換成UIButton的話,只會響應UIButton的touchUpInside,不會響應父View的tap事件。原因見蘋果官方說明:

In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer. This applies only to gesture recognition that overlaps the default action for a control, which includes:
* A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.
* A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
* A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.

解決方案
  • 在父View使用UIGesture的情況下,子view也使用UIGesture或者UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.控制元件。

總結

  1. UIGestureRecognizer 和 UITouch 都使用HitTest機制來尋找響應控制元件,hitest機制是本文一切衝突的基礎
  2. 避免同時使用多套手勢識別機制,在必須使用的時候,記得考慮上面提到的衝突
  3. gesture recognizer 不參與響應鏈(這篇文章並未涉及,但是由於其高優先順序,響應鏈對其根本無用武之地)
  4. 儘量不要直接使用UIControl