iOS觸摸事件處理

在開發(fā)過程中卓起,大家或多或少的都會碰到令人頭疼的手勢沖突問題,正好前兩天碰到一個類似的bug衅斩,于是借著這個機(jī)會了解了iOS中的事件傳遞與處理的相關(guān)內(nèi)容绑青,整理出來方便以后查閱诬像。

iPhone的成功,很大的一部分在于用戶可以以多種方式操縱他們的設(shè)備闸婴。大體上iOS的事件分為三類:觸摸事件(手勢操作),運動事件(搖一搖)芍躏,遠(yuǎn)程控制事件(耳機(jī)線控)邪乍,本文主要整理的是觸摸事件,對其它兩種就不多做介紹了,感興趣的同學(xué)可以自己查閱資料庇楞。

事件的生命周期

從手指觸摸屏幕榜配,觸摸事件的傳遞大概經(jīng)歷了3個階段,系統(tǒng)響應(yīng)階段-->SpringBoard.app處理階段-->前臺App處理階段吕晌,大致的流程如下圖:
uitouchflow.png
起始階段
  • cpu處于睡眠階段蛋褥,等待事件發(fā)生
  • 手指觸摸屏幕
系統(tǒng)響應(yīng)階段
  • 屏幕感應(yīng)到觸摸事件,并將感應(yīng)到的事件傳遞給IOKit(用來操作硬件和驅(qū)動的框架睛驳,這是一個私有API烙心,知道這個是干嘛的就行了)
  • IOKit.framework封裝整個觸摸事件為IOHIDEvent對象,直接通過mach port(Mach屬于硬件層乏沸,僅提供了諸如處理器調(diào)度淫茵、IPC進(jìn)程通信等非常少量的基礎(chǔ)服務(wù)。在Mach中蹬跃,所有的東西都是通過自己的對象實現(xiàn)的匙瘪,進(jìn)程、線程和虛擬內(nèi)存都被稱為“對象”蝶缀,Mach的對象間不能直接調(diào)用丹喻,只能通過消息傳遞的方式實現(xiàn)對象間的通信。消息是Mach中最基礎(chǔ)的概念翁都,消息在兩個端口(port)之間傳遞驻啤。mach port就是IPC進(jìn)程間通信的核心,更多內(nèi)容請查看這篇文章)轉(zhuǎn)發(fā)給SpringBoard.app荐吵。
SpringBoard.app處理階段
  • SpringBoard.app的主線程Runloop收到IOKit.framework轉(zhuǎn)發(fā)來的消息蘇醒骑冗,并觸發(fā)對應(yīng)mach port的Source1回調(diào)__IOHIDEventSystemClientQueueCallback()。
  • 如果SpringBoard.app監(jiān)測到有App在前臺(記為xxx.app)先煎,SpringBoard.app再通過mach port轉(zhuǎn)發(fā)給xxx.app贼涩,如果SpringBoard.app監(jiān)測到前臺沒有App運行,則SpringBoard.app進(jìn)入App內(nèi)部響應(yīng)階段薯蝎,觸發(fā)自身主線程runloop的Source0時間源的回調(diào)遥倦。

SpringBoard.app是一個系統(tǒng)進(jìn)程,可以理解為桌面系統(tǒng)占锯,可以統(tǒng)一管理和分發(fā)系統(tǒng)接收到的觸摸事件袒哥。

App內(nèi)部響應(yīng)階段
  • 前臺App主線程Runloop收到SpringBoard.app轉(zhuǎn)發(fā)來的消息而蘇醒,并觸發(fā)對應(yīng)mach port的Source1回調(diào)__IOHIDEventSystemClientQueueCallback()消略。
  • Source1回調(diào)內(nèi)部堡称,觸發(fā)Source0回調(diào)__UIApplicationHandleEventQueue()
  • Source0回調(diào)內(nèi)部,封裝IOHIDEvent為UIEvent艺演。
  • Source0回調(diào)內(nèi)部却紧,調(diào)用UIApplication的sendEvent:方法桐臊,將UIEvent傳給UIWindow,接下來就是尋找最佳響應(yīng)者的過程晓殊,也就是命中測試hit-testing断凶。
  • 尋找到最佳響應(yīng)者后,接下來就是事件在響應(yīng)鏈中的傳遞和響應(yīng)了巫俺。需要注意的是认烁,事件除了可以被響應(yīng)者處理之外,還有可能被手勢識別器或者target-action捕捉并處理介汹,這涉及到一個優(yōu)先級的問題却嗡。如果觸摸事件在響應(yīng)鏈中沒有找到能夠響應(yīng)該事件的對象,最終將被釋放痴昧。
  • 事件被處理或者釋放之后稽穆,runloop如果沒有其他事件進(jìn)行處理,將會再次進(jìn)入休眠狀態(tài)赶撰。

Source0和Source1都可用于線程(或進(jìn)程)交互舌镶,但交互的形式有所不同,Source1監(jiān)聽端口豪娜,當(dāng)端口有消息到達(dá)時餐胀,響應(yīng)的Source1就會被觸發(fā)回調(diào),完成響應(yīng)的操作瘤载;而Source0并不監(jiān)聽端口否灾,讓Source0執(zhí)行回調(diào)需要手動標(biāo)記Source0為待處理狀態(tài),還需要呼醒Source0所在的Runloop鸣奔。從Source1和Source0的交互方式了解到墨技,Source1的交互會主動呼醒所在的Runloop,而Source0的交互則需要依賴其他線程來呼醒Source0所在的Runloop挎狸。一次Runloop只能執(zhí)行一個Source1的回調(diào)扣汪,但可以執(zhí)行多個待處理的Source0的回調(diào)。

尋找事件的最佳響應(yīng)者(Hit-Testing)

能夠響應(yīng)觸摸事件的例如UIView锨匆,UIButton崭别,UIViewController,UIApplication恐锣,Appdelegate等都繼承自UIResponder類茅主,一個頁面上通常會有許許多多個這種類型的對象,都可以對點擊事件作出響應(yīng)土榴。為了避免沖突诀姚,這就需要有一個先后順序,也就是響應(yīng)的優(yōu)先級鞭衩。Hit-Testing的目的就是找到具有最高優(yōu)先級的響應(yīng)對象学搜。
尋找的具體流程如下:

  1. UIApplication首先將事件隊列中的事件取出娃善,傳遞給窗口對象论衍。如果有多個窗口瑞佩,則優(yōu)先詢問windows數(shù)組的最后一個窗口。
  2. 如果窗口不能響應(yīng)事件坯台,則將事件傳遞給倒數(shù)第二個窗口炬丸,以此類推。如果窗口能夠響應(yīng)事件蜒蕾,則再依次詢問該窗口的子視圖稠炬。
  3. 重復(fù)步驟2。
  4. 若視圖的所有子視圖均不是最佳響應(yīng)者咪啡,則自身就是最合適的響應(yīng)者首启。
    另外需要注意的是,一下幾種狀態(tài)的視圖無法響應(yīng)事件:
  • 不允許交互的視圖:userInteractionEnabled = NO
  • 隱藏的視圖:hidden = YES
  • 透明度alpha<0.01的視圖

怎么樣驗證一下上面所說的Hit-Testing的順序呢撤摸,看一下UIView的API毅桃,里面會有一個hitTest:withEvent:方法,這個方法的主要作用就是查詢并返回事件在當(dāng)前視圖中的響應(yīng)者准夷,每個被詢問到的視圖對象都會調(diào)用這個方法來返回當(dāng)前視圖層的響應(yīng)者钥飞。

  • 如果當(dāng)前視圖無法響應(yīng)事件,則返回nil衫嵌。
  • 如果當(dāng)前視圖可以響應(yīng)事件读宙,但子視圖不能響應(yīng)事件,則返回自身作為當(dāng)前視圖的響應(yīng)者楔绞。
  • 如果當(dāng)前視圖可以響應(yīng)事件结闸,同時有子視圖可以響應(yīng)事件,則返回該子視圖作為當(dāng)前視圖的響應(yīng)者酒朵。

所以我們可以根據(jù)通過觀察該方法的調(diào)用順序桦锄,來確定Hit-Testing的順序。


屏幕快照 2017-11-27 下午2.32.23.png

如圖所示耻讽,A視圖上面添加了子視圖B和C察纯,B上面添加了子視圖D,C上面添加了子視圖E和F针肥。創(chuàng)建一個繼承自UIView的類HTView饼记,重寫hitTest:withEvent:方法:

@interface HTView : UIView
@property (nonatomic, strong) NSString *name; //視圖的名字
@end

@implementation HTView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"進(jìn)入%@視圖-%s", self.name, __func__);
    UIView *view = [super hitTest:point withEvent:event];
    NSLog(@"離開%@視圖-%s", self.name, __func__);
    [圖片上傳中...(屏幕快照 2017-11-27 下午3.58.14.png-334090-1511769555186-0)]

return view;
}
@end

在ViewController中添加如下代碼:

#import "ViewController.h"
#import "HTView.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet HTView *aView;
@property (weak, nonatomic) IBOutlet HTView *bView;
@property (weak, nonatomic) IBOutlet HTView *cView;
@property (weak, nonatomic) IBOutlet HTView *dView;
@property (weak, nonatomic) IBOutlet HTView *eView;
@property (weak, nonatomic) IBOutlet HTView *fView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.aView.name = @"A";
    self.bView.name = @"B";
    self.cView.name = @"C";
    self.dView.name = @"D";
    self.eView.name = @"E";
    self.fView.name = @"F";
}
@end

點擊E視圖,打印的結(jié)果如下:


屏幕快照 2017-11-27 下午4.16.08.png

由打印的結(jié)果可知:

  1. 事件首先傳遞給視圖A慰枕。
  2. A判斷自身能響應(yīng)事件具则,繼續(xù)從后向前遍歷A的子視圖,因為C比B后添加具帮,因此首先傳遞給C博肋。
  3. C判斷自身能響應(yīng)事件低斋,繼續(xù)從后向前遍歷C的子視圖,因為F比E后添加匪凡,因此首先傳遞給F膊畴。
  4. F判斷自身不能響應(yīng)事件眉反,C又將事件傳遞給E崭参。
  5. E判斷自身能響應(yīng)事件,同時E已經(jīng)沒有子視圖层坠,因此最終E就是最佳響應(yīng)者衬衬。

(這里有一個問題买猖,為什么遍歷視圖的時候需要從后往前遍歷呢?為什么B和C都是A的子視圖滋尉,判斷出了C視圖能響應(yīng)事件之后玉控,B視圖沒有繼續(xù)調(diào)用hitTest:withEvent:方法呢?)

那么視圖又是怎么判斷自身是否可以響應(yīng)事件的呢狮惜?答案是通過poingInside:withEvent這個方法來判斷觸摸點是否在視圖的坐標(biāo)范圍內(nèi)高诺。那么結(jié)合上面的hitTest調(diào)用的相關(guān)知識來看,hitTest:withEvent方法的大概實現(xiàn)已經(jīng)呼之欲出了:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 先判斷視圖是否處于不能響應(yīng)事件的3種狀態(tài)
    if (self.userInteractionEnabled == NO || self.hidden || self.alpha < 0.01) {
        return nil;
    }
    // 判斷觸摸點是否在視圖的坐標(biāo)范圍內(nèi)
    if ([self pointInside:point withEvent:event] == NO) {
        return nil;
    }
    // 從后向前遍歷視圖的子視圖
    for (int i = (int)self.subviews.count - 1; i >= 0; i--) {
        UIView *subView = self.subviews[i];
        // 坐標(biāo)轉(zhuǎn)換讽挟,把觸摸點的位置轉(zhuǎn)換為子視圖坐標(biāo)系下的坐標(biāo)
        CGPoint subPoint = [self convertPoint:point toView:subView];
        // 對子視圖進(jìn)行Hit-Testing
        UIView *subHTView = [subView hitTest:subPoint withEvent:event];
        // 如果子視圖有最佳響應(yīng)者懒叛,返回該最佳響應(yīng)者視圖,結(jié)束循環(huán)
        if (subHTView) {
            return subHTView;
        }
    }
    // 如果子視圖中沒有最佳響應(yīng)者耽梅,返回自己
    return self;
}

重新點擊薛窥,發(fā)現(xiàn)視圖仍然可以正常響應(yīng)點擊事件,證明我們所寫的實現(xiàn)與系統(tǒng)的方法基本相同眼姐。這里我們就可以回答上面括號里面的問題了诅迷,為什么要從后往前遍歷呢?因為數(shù)組里面后面的視圖是后添加的众旗,后添加的視圖一般都是在視圖的上層罢杉,會把先添加的視圖遮擋,我們自然不會想要去點擊被遮擋住的位置贡歧。為什么B視圖沒有調(diào)用hitTest:withEvent:方法呢滩租?因為已經(jīng)確定觸摸點在C視圖上了。如果B和C沒有重疊部分利朵,自然不用再判斷B視圖能否響應(yīng)律想,如果有重疊部分,后添加的C自然是在上層绍弟,所以C優(yōu)先響應(yīng)技即,也不會再對B視圖進(jìn)行判斷。
我們通過這段代碼還可以解釋另外一種現(xiàn)象樟遣,子視圖超出了父視圖的范圍而叼,點擊子視圖在父視圖之外的部分沒有反應(yīng)身笤。這是因為在進(jìn)行Hit-Testing的時候,父視圖就已經(jīng)判斷自己不能響應(yīng)事件了葵陵,自然不會再去詢問子視圖是否能夠響應(yīng)事件液荸。

如果碰到這種需求怎么辦?比如說tabBar中間的按鈕凸起


FD64ADCA-17B2-496B-AC09-5A7AEEDFF183.png

這時候就需要重寫父視圖的pointInside:withEvent:方法了埃难,在tabBar中判斷當(dāng)前觸摸位置是否在中間凸起的按鈕的坐標(biāo)范圍內(nèi)莹弊,如果在涤久,就返回YES涡尘。這樣得以讓觸摸事件傳遞到中間的按鈕上,并確定按鈕為最佳響應(yīng)者响迂。代碼如下:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    //將觸摸點坐標(biāo)轉(zhuǎn)換到在circleButton上的坐標(biāo)
    CGPoint subPoint = [self convertPoint:point toView:self.circleButton];
    //若觸摸點circleButton上則返回YES
    if ([self.circleButton pointInside:subPoint withEvent:event])       {
        return YES;
    }
    //否則返回默認(rèn)的操作
    return [super pointInside:point withEvent:event];
}

這里還有另一個問題考抄,為什么Hit-Testing過程進(jìn)行了兩次?
剛開始的猜想是兩次執(zhí)行時的參數(shù)event不一樣蔗彤,點擊A視圖的父視圖川梅,打印event對象:


1A71FACD-C456-4EA5-8290-FD9FA5805341.png
1A71FACD-C456-4EA5-8290-FD9FA5805341.png

從結(jié)果可以看到,兩次的event對象地址一樣然遏,且allTouches集合里面沒有UITouch對象(看到有的簡友提到兩次調(diào)用的原因是UITouch對象的狀態(tài)不同贫途,一次是begin,一次是end待侵,這一點我持懷疑態(tài)度丢早,因為沒法驗證⊙砬悖看上面提到的App響應(yīng)階段的操作怨酝,source0把IOHIDEvent對象封裝成了UIEvent對象,再結(jié)合這里hitTest:withEvent:方法里面沒有UITouch對象那先,然后我們還可以看一下touchesBegan方法里面是有touches對象的农猬,我猜測UITouch對象是依據(jù)UIEvent對象的某些屬性生成的,這個過程發(fā)生在Hit-Testing過程之后售淡。既然UITouch對象都沒有生成斤葱,那就更談不上UITouch狀態(tài)的變化了,當(dāng)然這里是我的臆想揖闸,希望有大神看到之后進(jìn)行指正)揍堕。然后我在hitTest:withEvent:里面打了一個斷點,在event里面發(fā)現(xiàn)了這么一個東西:


20248863-DD94-418F-889F-1C94BABCF5F9.png

類型是_IOHIDEvent楔壤。這不就是最開始封裝成UIEvent那個類嗎鹤啡,觸摸事件的相關(guān)信息應(yīng)該就儲存在這個成員變量里面。這是一個私有成員變量蹲嚣,直接取是取不出來的递瑰,不過這難不倒我們祟牲,我們有runtime可以取。在hitTest:withEvent:里面添加如下代碼:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    Ivar ivar = class_getInstanceVariable([event class], "_hidEvent");
    NSLog(@"%@", object_getIvar(event, ivar));
    return [super hitTest:point withEvent:event];
}

點擊A視圖的父視圖(這里只會對A進(jìn)行l(wèi)兩次hitTest抖部,方便我們查看)说贝,第一次打印:

0B0483B1-E4A8-4EBB-859F-2A34159A7E4D.png

第二次打由骺拧:

7646AC0D-018F-4E5A-9D87-2F45ACE6F813.png

然后滿懷希望的一項一項比對里面的信息乡恕,貌似除了Total Latency(總延時)之外都是一樣的,而且這里面好多字段我也不知道什么意思俯萎,找了半天也沒有找到_IOHIDEvent的API(有這方面資料的同學(xué)請不吝賜教)傲宜。按照這種方式把event里面的成員變量一個個打印,大部分都是空數(shù)組或者空字典夫啊『洌看來這兩次hitTest:withEvent:的區(qū)別不在于參數(shù)上面。那么會不會在方法的調(diào)用順序上面有區(qū)別呢撇眯?依然是點擊A視圖的父視圖报嵌,看一下方法調(diào)用棧:

第一次調(diào)用棧
第二次調(diào)用棧

可以看到兩次調(diào)用的不同就在于紅框圈出來的部分。第一次有對UIWindow進(jìn)行Hit-Testing熊榛,第二次沒有锚国,而是直接對UIView進(jìn)行了Hit-Testing。至于其中的原因玄坦,我也不知道血筑。。营搅。云挟。。扯了那么多转质,也沒得出個結(jié)果园欣,不要打我。

事件的響應(yīng)及在響應(yīng)鏈中的傳遞

經(jīng)歷Hit-Testing后休蟹,UIApplication已經(jīng)知道事件的最佳響應(yīng)者是誰了沸枯,接下來要做的兩件事情就是:

  1. 將事件傳遞給最佳響應(yīng)者響應(yīng)。
  2. 事件沿著響應(yīng)鏈傳遞赂弓,直到有UIResponder對象對此事件負(fù)責(zé)绑榴。
事件響應(yīng)的前奏

因為最佳響應(yīng)者具有最高的事件響應(yīng)優(yōu)先級,因此UIApplication會先將事件傳遞給它供其響應(yīng)盈魁。首先翔怎,UIApplication將事件通過sendEvent:傳遞給事件所屬的window,window同樣通過sendEvent:再將事件傳遞給最佳響應(yīng)者。在自定義的View里面重寫touchesBegan:方法赤套,打上斷點飘痛,可以看到調(diào)用棧如下:

touchesBegan調(diào)用棧

那么UIApplication和UIWindow又是怎么知道應(yīng)該把事件發(fā)送給哪個視圖的呢?我們可以看一下touches里面的UITouch對象的屬性容握。里面有window和view兩個字段宣脉,分別代表事件分發(fā)的UIWindow和最佳響應(yīng)者的地址(需要注意的是這個地址并不代表最終響應(yīng)事件的UIResponder地址,只是會最先分發(fā)給它剔氏,它也可以不對事件作出響應(yīng))塑猖。還有一個字段gestureRecongnizers,這里面存儲了響應(yīng)鏈上的視圖上面添加的手勢(這里面的東西在后面講到的手勢識別器會有涉及)谈跛。


FEA6ED47-036A-4086-94D5-802D11DFF86D.png
事件的響應(yīng)

響應(yīng)者鏈上面的每個響應(yīng)者都是繼承于UIResponder的對象羊苟,每個UIResponder對象都默認(rèn)實現(xiàn)了4個響應(yīng)觸摸事件的方法:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event币旧;

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event践险;

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

系統(tǒng)默認(rèn)的方法不對事件做任何處理吹菱,只是將事件沿著響應(yīng)者鏈傳遞。如果想要截獲事件進(jìn)行自定義的響應(yīng)操作彭则,就要重寫相關(guān)的方法鳍刷。例如:重寫touchesMoved方法實現(xiàn)簡單的視圖拖動。

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint point = [touch locationInView:self.view];
    CGPoint prePoint = [touch previousLocationInView:self.view];
    self.aView.transform = CGAffineTransformTranslate(self.aView.transform, point.x - prePoint.x, point.y - prePoint.y);
}

該方法在父視圖和子視圖中重寫會有不同的效果俯抖,如果在父視圖中重寫输瓜,點擊位置在你要移動的子視圖之外也可以移動。如果在子視圖中重寫芬萍,則點擊位置需要在子視圖的范圍之內(nèi)才可以移動尤揣。

事件在響應(yīng)者鏈上的傳遞

前面一直在提最佳響應(yīng)者,之所以稱為"最佳"柬祠,是因為其具備響應(yīng)事件的最高優(yōu)先權(quán)(響應(yīng)鏈頂端的男人)北戏。最佳響應(yīng)者首先接收到事件,然后便擁有了對事件的絕對控制權(quán):它既可以選擇獨吞這個事件漫蛔,也可以將這個事件往下傳遞給其它響應(yīng)者嗜愈,這個由響應(yīng)者構(gòu)成的鏈就稱之為響應(yīng)者鏈。
需要注意的是莽龟,上面也說到了事件的傳遞蠕嫁,這與此處所說的事件的傳遞有本質(zhì)的區(qū)別。上面所說的事件的傳遞的目的是為了尋找事件的最佳響應(yīng)者毯盈,是自下而上的傳遞剃毒。而這里的事件傳遞的目的是響應(yīng)者對事件作出響應(yīng),這個過程是自上而下的,前者為"尋找"赘阀,后者為"響應(yīng)"陪拘。
響應(yīng)者對于事件的操作方式:
響應(yīng)者對于事件的攔截以及傳遞都是通過touchesBegan:withEvent:方法控制的,該方法的默認(rèn)實現(xiàn)是將事件沿著默認(rèn)的響應(yīng)鏈往下傳遞纤壁。(如果你在不同的UIResponder對象上面都聲明了touchMoved方法左刽,那么這些對象都可以執(zhí)行該方法,因為touchesBegan方法默認(rèn)是把事件沿著響應(yīng)鏈傳遞的酌媒。如果只想讓一個對象響應(yīng)touchesMoved方法欠痴,需要重寫touchesBegan方法以攔截事件)
響應(yīng)者對于接收到的事件有3種操作:

  • 不攔截,默認(rèn)操作
    事件會自動沿著默認(rèn)的響應(yīng)鏈向下傳遞
  • 攔截秒咨,不再往下分發(fā)事件
    重寫touchesBegan:withEvent:方法進(jìn)行事件處理喇辽,不調(diào)用父類的touchesBegan:withEvent:方法
  • 攔截,繼續(xù)往下分發(fā)事件
    重寫touchesBegan:withEvent:進(jìn)行事件處理雨席,同時調(diào)用父類的touchesBegan:withEvent:將事件往下傳遞
響應(yīng)鏈中的事件傳遞規(guī)則:

每一個響應(yīng)者對象(UIResponder對象)都有一個nextResponder方法菩咨,用于獲取響應(yīng)鏈中當(dāng)前對象的下一個響應(yīng)者。因此陡厘,一旦事件的最佳響應(yīng)者確定了抽米,這個事件所處的響應(yīng)鏈就確定了。

對于響應(yīng)者對象糙置,默認(rèn)的nextResponder實現(xiàn)如下:

  • UIView
    若視圖是控制器的根視圖云茸,則其nextResponder為控制器對象;否則谤饭,其nextResponder為父視圖标捺。
  • UIViewController
    若控制器的視圖是window的根視圖,則其nextResponder為窗口對象揉抵;若控制器是從別的控制器present出來的亡容,則其nextResponder為presenting view controller。
  • UIWindow
    nextResponder為UIApplication對象
  • UIApplication
    若當(dāng)前應(yīng)用的app delegate是一個UIResponder對象冤今,且不是UIView闺兢、UIViewController或app本身,則UIApplication的nextResponder為app delegate
  • AppDelegate
    nextResponder為nil

792F789A-7322-4E73-A92D-513340F7AFB4.png

上圖是官網(wǎng)對于響應(yīng)鏈的示例展示辟汰,若觸摸發(fā)生在UITextField上列敲,則事件的傳遞順序是:
UITextField->UIView->UIView->UIViewController->UIWindow->UIApplication->UIApplicationDelegate
圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的,則其nextResponder為UIViewController對象帖汞;若是直接add在UIWindow上的戴而,則其nextResponder為UIWindow對象。

可以通過重寫UIResponder對象的- (nullable UIResponder*)nextResponder;方法改變響應(yīng)者鏈翩蘸,但是UIResponder對象的nextResponder屬性是只讀屬性所意,不能直接賦值。

UIGestureRecognizer

在iOS中有六種手勢操作:
UITapGestureRecognizer 點按手勢
UIPinchGestureRecognizer 捏合手勢
UIPanGestureRecognizer 拖動手勢
UISwipeGestureRecognizer 輕掃手勢,支持四個方向的輕掃扶踊,但是不同的方向要分別定義輕掃手勢
UIRotationGestureRecognizer 旋轉(zhuǎn)手勢
UILongPressGestureRecognizer 長按手勢
所有的手勢操作都繼承于UIGestureRecognizer泄鹏,這個類本身不能直接使用。這個類中定義了這幾種手勢公有的一些屬性和方法秧耗。


CFFB9CE8-59D3-4DDB-B9D7-70D4E77702A8.png
手勢狀態(tài)

這里著重解釋一下上表中手勢狀態(tài)這個對象备籽。在六種手勢識別中,只有一種手勢是離散手勢分井,它就是UITapGestureRecognizer车猬。離散手勢的特點就是一旦識別就無法取消,而且只會調(diào)用一次手勢操作(初始化手勢時指定的觸發(fā)方法)尺锚。其它五種手勢是連續(xù)手勢珠闰,連續(xù)手勢的特點就是會多次調(diào)用手勢操作事件,而且在連續(xù)手勢識別后可以取消手勢瘫辩。從下面兩圖中可以看出兩者調(diào)用操作事件的次數(shù)是不同的:


471B0C89-9B09-4A78-8607-2916464989B6.png

在iOS中將手勢狀態(tài)分為如下幾種:


2D616318-E45F-43B0-B651-CB70C5037F76.png
  • 對于離散型手勢UITapGestureRecognizer要么被識別伏嗜,要么失敗,點按(假設(shè)點按次數(shù)設(shè)置為1伐厌,并且沒有添加長按手勢)下去一次不松開則此時什么也不會發(fā)生承绸,松開手指立即識別并調(diào)用操作事件,并且狀態(tài)為3(已完成)弧械。
  • 但是連續(xù)手勢要復(fù)雜一些八酒,就拿旋轉(zhuǎn)手勢來說,如果兩個手指點下去不做任何操作刃唐,此時并不能識別手勢(因為我們還沒有旋轉(zhuǎn))但是其實已經(jīng)出發(fā)了觸摸開始事件,此時處于狀態(tài)0界轩;如果此時旋轉(zhuǎn)會被識別画饥,也就會調(diào)用對應(yīng)的操作事件,同時狀態(tài)變成1(手勢開始)浊猾,但是狀態(tài)1只有一瞬間抖甘;緊接著變成狀態(tài)2(因為我們的旋轉(zhuǎn)需要持續(xù)一會),并且重復(fù)調(diào)用操作事件(如果在事件中打印狀態(tài)會重復(fù)打印2)葫慎;松開手指衔彻,此時狀態(tài)變?yōu)?,并調(diào)用1次操作事件偷办。

為了大家更好的理解這個狀態(tài)的變化艰额,不妨在操作事件中打印事件狀態(tài),會發(fā)現(xiàn)在操作事件中的狀態(tài)永遠(yuǎn)不可能為0(默認(rèn)狀態(tài))椒涯,因為只要調(diào)用此事件說明已經(jīng)被識別了柄沮。前面也說過,手勢識別從根本還是調(diào)用觸摸事件而完成的,連續(xù)手勢之所以會發(fā)生狀態(tài)轉(zhuǎn)換完全是由于觸摸事件中的移動事件造成的祖搓,沒有移動事件也就不存在這個過程中狀態(tài)變化狱意。
大家通過蘋果官方的分析圖再理解一下上面說的內(nèi)容:


313E155C-7FD0-4685-A297-812FB3870CC4.png

手勢的具體使用這里就不贅述了,先來看一下下面幾種使用手勢時產(chǎn)生沖突的情況拯欧。

手勢和UIResponder之間的沖突

先看一個簡單的例子:


BA814FD0-7F14-4BE1-A902-9C1C1F52BB6C.png

控制器的視圖上add了一個View記為YellowView详囤,并綁定了一個單擊手勢識別器。

@interface ViewController ()
@property (weak, nonatomic) IBOutlet YellowView *yellowView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
    [self.yellowView addGestureRecognizer:tap];
}

- (void)tap:(id)sender {
    NSLog(@"tap");
}
@end

點擊YellowView镐作,日志打印如下:

FAD8DBA7-D76E-4C4C-AA7A-A71CE1D648BA.png

從日志上看出YellowView最后cancel了對觸摸事件的響應(yīng)藏姐,而正常應(yīng)當(dāng)是觸摸結(jié)束后,YellowView的touchesEnded:withEvent:方法被調(diào)用才對滑肉。另外包各,期間還執(zhí)行了手勢識別器綁定的action。對于這種現(xiàn)象靶庙,官方文檔上有這么一段描述:

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled. The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties:

這段描述的意思是:UIWindow會先把touch事件分發(fā)給手勢識別器问畅,然后再分發(fā)給hit-tested view,如果一個手勢識別器分析了這一系列的點擊事件之后沒有識別出該手勢六荒,hit-tested view將會接收完整的點擊事件护姆。如果手勢識別器識別了該手勢,hit-tested view將會取消這次點擊掏击。由此可以看出:手勢識別器比UIResponder具有更高的事件響應(yīng)優(yōu)先級卵皂。

按照這個解釋,UIWindow在將事件傳遞給hit-tested view即YellowView之前砚亭,先傳遞給了手勢識別器灯变。手勢識別器成功識別了該事件,通知application取消YellowView對事件的響應(yīng)捅膘。

然而看日志添祸,卻是YellowView的touchesBegan:withEvent:先調(diào)用了,既然手勢識別器先響應(yīng)寻仗,不應(yīng)該上面的action先執(zhí)行嗎刃泌?實際上這個認(rèn)知是錯誤的。手勢識別器的action的調(diào)用時機(jī)并不是手勢識別器接收到事件的時機(jī)署尤,而是手勢識別器成功識別事件后的時機(jī)耙替,即手勢識別器的狀態(tài)變?yōu)閁IGestureRecognizerStateRecognized。要證明UIWindow先將事件傳遞給了手勢識別器曹体,還是需要看手勢識別器中這四個熟悉的方法的調(diào)用結(jié)果俗扇。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

不過不要誤會,UIGestureRecognizer并不繼承于UIResponder類混坞,他們只是方法名相同而已狐援。
這樣钢坦,我們就可以自定義一個繼承自UITapGestureRecognizer的子類,重寫這四個方法啥酱,觀察事件分發(fā)的順序爹凹。上面的四個分發(fā)聲明在UIGestureRecognizerSubclass.h中,所以想要重寫的話需要引入頭文件#import <UIKit/UIGestureRecognizerSubclass.h>镶殷。

#import "TapGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
@implementation TapGestureRecognizer

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 這里需要調(diào)用一下父類的touchesBegan方法禾酱,否則事件會被攔截消耗掉
    [super touchesBegan:touches withEvent:event];
    NSLog(@"%s", __func__);
}

@end

點擊YellowView,輸出以下內(nèi)容:


F5295756-A948-4B7D-9F7E-EDB5A7B3B488.png

可以看到绘趋,確實是手勢識別器先接收到了事件颤陶,然后hit-tested view接收到事件佩伤。接著手勢識別器識別了手勢蕊温,執(zhí)行action朱监,再由Application取消了YellowView對事件的響應(yīng)扮休。

那么UIWindow是怎么知道要把事件傳遞給哪些手勢識別器的呢?我們上面有一張圖提到過蕉汪,這些手勢識別器的相關(guān)信息都儲存在UITouch對象的gestureRecognizers里面盟蚣,這是一個數(shù)組刺覆,可以儲存多個手勢識別器绽族。

UIGestureRecognizer分為離散型手勢和持續(xù)型手勢姨涡,我們上面的demo用的是離散型手勢,那么如果是持續(xù)型手勢又會有什么樣的結(jié)果呢吧慢?我們把UITapGestureRecognizer用UIPanGestureRecognizer替換涛漂,然后在YellowView上面執(zhí)行一次滑動,輸出結(jié)果如下:


4D526623-3469-45E1-9166-C49F74A7F581.png

在一開始滑動的過程中检诗,手勢識別器處在識別手勢階段匈仗,滑動產(chǎn)生的連續(xù)事件既會傳遞給手勢識別器又會傳遞給YellowView,因此YellowView的touchesMoved:withEvent:在開始一段時間內(nèi)會持續(xù)調(diào)用逢慌;當(dāng)手勢識別器成功識別了該滑動手勢時锚沸,手勢識別器的action開始調(diào)用,同時通知Application取消YellowView對事件的響應(yīng)涕癣。之后僅由滑動手勢識別器接收事件并響應(yīng),YellowView不再接收事件前标。另外坠韩,在滑動的過程中,若手勢識別器未能識別手勢炼列,則事件在觸摸滑動過程中會一直傳遞給hit-tested view只搁,直到觸摸結(jié)束。

手勢識別器的3個屬性
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;

在介紹這三個屬性之前俭尖,先來總結(jié)一下手勢識別器與UIResponder對于事件響應(yīng)的聯(lián)系:
當(dāng)觸摸發(fā)生或者觸摸的狀態(tài)發(fā)生變化時氢惋,UIWindow都會傳遞事件尋求響應(yīng)洞翩。

  • UIWindow先將綁定了觸摸對象的事件傳遞給觸摸對象上綁定的手勢識別器,再發(fā)送給觸摸對象對應(yīng)的hit-tested view焰望。
  • 手勢識別器識別手勢期間骚亿,若觸摸對象的觸摸狀態(tài)發(fā)生變化,事件都是先發(fā)送給手勢識別器再發(fā)送給hit-test view熊赖。
  • 手勢識別器若成功識別了手勢来屠,則通知Application取消hit-tested view對于事件的響應(yīng),并停止向其發(fā)送事件震鹉。
  • 若手勢識別器未能識別手勢俱笛,而此時觸摸并未結(jié)束,則停止向手勢識別器發(fā)送事件传趾,僅向hit-tested view發(fā)送事件迎膜。
  • 若手勢識別器未能識別手勢,且此時觸摸已結(jié)束浆兰,則向hit-tested view發(fā)送end狀態(tài)的touch事件以停止對事件的響應(yīng)磕仅。

cancelsTouchInView

默認(rèn)為YES。表示當(dāng)手勢識別器成功識別了手勢之后镊讼,會通知Application取消響應(yīng)鏈對事件的響應(yīng)宽涌,并不再傳遞事件給hit-tested view。若設(shè)置成NO蝶棋,表示手勢識別成功后不取消響應(yīng)鏈對事件的響應(yīng)卸亮,事件依舊會傳遞給hit-test view。把上面的demo中手勢的cancelsTouchInView屬性設(shè)置為NO玩裙,打印輸出結(jié)果:


2AFFDDBD-4425-40EE-882D-6DA7BC4F5138.png

可以看到兼贸,即便滑動手勢識別器識別了手勢,Application也會依舊發(fā)送事件給YellowView吃溅。

delaysTouchesBegan

默認(rèn)為NO溶诞。默認(rèn)情況下手勢識別器在識別手勢期間,當(dāng)觸摸狀態(tài)發(fā)生改變時决侈,Application都會將事件分別傳遞給手勢識別器和hit-tested view螺垢;若設(shè)置成YES,則表示手勢識別器再識別手勢期間赖歌,截斷事件枉圃,即不會將事件發(fā)送給hit-tested view。把上面demo中的手勢識別器的delaysTouchesBegan設(shè)置為YES庐冯。


36D3BB79-B0A9-499D-8BEE-B857914B7062.png

因為滑動手勢識別器在識別期間孽亲,事件不會傳遞給YellowView,因此期間YellowView的touchesBegan:withEvent:和touchesMoved:withEvent:都不會被調(diào)用展父。而后滑動手勢識別器成功識別了手勢返劲,也就獨吞了事件玲昧,不會再傳遞給YellowView。因此只打印了手勢識別器成功識別手勢后的action調(diào)用篮绿。

delaysTouchesEnded

默認(rèn)為YES孵延。當(dāng)手勢識別失敗時,若此時觸摸已經(jīng)結(jié)束搔耕,會延遲一小段時間再調(diào)用響應(yīng)者的touchesEnded:withEvent:隙袁。若設(shè)置成NO,則在手勢識別失敗時會立即通知Application發(fā)送狀態(tài)為end的touch事件給hit-tested view以調(diào)用touchesEnded:withEvent:結(jié)束事件響應(yīng)弃榨。

同一個視圖中的不同手勢之間的沖突

如果在同一個視圖上添加不同的手勢時菩收,也有可能會發(fā)生沖突。照例先上代碼:

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    [self.imageView addGestureRecognizer:pan];

    UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    [self.imageView addGestureRecognizer:swipe];
}

- (void)pan:(UIPanGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
        CGPoint point = [gestureRecognizer translationInView:self.imageView];
        self.imageView.transform = CGAffineTransformMakeTranslation(point.x, point.y);
    }else if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
        [UIView animateWithDuration:0.3 animations:^{
            self.imageView.transform = CGAffineTransformIdentity;
        }];
    }
    NSLog(@"%s", __func__);
}

- (void)swipe:(UISwipeGestureRecognizer *)gestureRecognizer
{
    static BOOL flag = NO;
    self.imageView.image = flag ? [UIImage imageNamed:@"b4723daa4e8f8fc434bc2e79a1bc4d8c"] : [UIImage imageNamed:@"a96703f39203a4e650f9e24655dc4864"];
    flag = !flag;
    NSLog(@"%s", __func__);
}

@end

代碼很簡單鲸睛,最初我們的目的是在imageView上面添加一個拖動手勢娜饵,一個輕掃手勢。拖動的時候改變圖片的位置官辈,輕掃的時候切換圖片箱舞。


QQ20171205-111141.gif

D6004995-02E2-46CE-A53E-F686D6784DF6.png

可以看到,盡管我減小了輕掃的幅度拳亿,加快了速度晴股,輕掃手勢依然沒有起作用,就是因為輕掃和拖動這兩個手勢起了沖突肺魁。沖突的原因很簡單电湘,拖動手勢的操作事件是在手勢的開始狀態(tài)(狀態(tài)1)識別執(zhí)行的,而輕掃手勢的操作事件只有在手勢結(jié)束狀態(tài)(狀態(tài)3)才能執(zhí)行鹅经,因此輕掃手勢就作為了犧牲品沒有被正確識別寂呛。要解決這個沖突可以利用requireGestureRecognizerToFail:方法來完成,這個方法可以指定某個手勢執(zhí)行的前提是另一個手勢識別失敗瘾晃。

這里我們把拖動手勢設(shè)置為輕掃手勢識別失敗之后執(zhí)行贷痪,這樣一來我們手指輕輕滑動時系統(tǒng)會優(yōu)先考慮輕掃手勢,如果最后發(fā)現(xiàn)該操作不是輕掃蹦误,那么就會執(zhí)行拖動劫拢。

只需要添加代碼:

[pan requireGestureRecognizerToFail:swipe];

運行效果:


QQ20171205-135252.gif
不同視圖上的手勢沖突

在上面響應(yīng)者鏈的學(xué)習(xí)中,我們知道了UIResponder響應(yīng)事件的時候是有優(yōu)先級的强胰,上層觸摸事件執(zhí)行后就不再向下傳播尚镰。默認(rèn)情況下手勢也是類似的,先識別的手勢會阻斷手勢識別操作繼續(xù)傳播哪廓。下面我們用代碼驗證一下:

我們在控制器的視圖上面添加一個黃色的子視圖,然后在黃色視圖上面添加一個自定義的滑動手勢初烘,在控制器的view上面也添加一個自定義的滑動手勢涡真。在自定義的滑動手勢里面重寫touchBegan:withEvent:這四個相關(guān)的方法.

@interface GestureRecognizer : UIPanGestureRecognizer
@property (nonatomic, strong) NSString *panName;
@end

@implementation GestureRecognizer

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesCancelled:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

@end

// ViewController

@interface ViewController ()
@property (weak, nonatomic) IBOutlet YellowView *yellowView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    GestureRecognizer *pan = [[GestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    pan.panName = @"第一個";
    [self.yellowView addGestureRecognizer:pan];

    GestureRecognizer *panBottom = [[GestureRecognizer alloc] initWithTarget:self action:@selector(panBottom:)];
    panBottom.panName = @"第二個";
    [self.view addGestureRecognizer:panBottom];
}

- (void)pan:(UIPanGestureRecognizer *)gestureRecognizer
{
    NSLog(@"%s", __func__);
}

- (void)panBottom:(UIGestureRecognizer *)gestureRecognizer{
    NSLog(@"%s", __func__);
}

在黃色視圖上滑動分俯,輸出以下結(jié)果:


ACB1FA17-AC4B-4B57-ACB1-84BBBF83221F.png

可以看到,在手勢識別期間哆料,UIWindow會依次向兩個手勢識別器和hit-test view發(fā)送事件缸剪,而手勢識別成功后,UIWindow停止向控制器視圖上面添加的滑動手勢發(fā)送事件东亦,導(dǎo)致其action無法被調(diào)用杏节,從而產(chǎn)生沖突。為什么停止接收事件的是第二個滑動手勢呢典阵?還記得我們上面提到過的UITouch里面的數(shù)組gestureRecognizers嗎奋渔,手勢識別的優(yōu)先級跟數(shù)組的數(shù)據(jù)是保持一致的,和響應(yīng)者鏈的響應(yīng)順序也有點類似壮啊。

4AE1F41A-62F1-4DB3-8CF4-D71F21AF2547.png

我們可以看到嫉鲸,第一個手勢儲存在數(shù)組的最前面,他的優(yōu)先級比較高歹啼,所以會首先被響應(yīng)玄渗。

那么如何讓兩個有層次關(guān)系并且都添加了手勢的控件都能正確識別手勢呢?答案就是利用手勢代理的gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:代理方法狸眼。
蘋果官方是這么描述這個方法的:

Asks the delegate if two gesture recognizers should be allowed to recognize gestures simultaneously.
This method is called when recognition of a gesture by either gestureRecognizer or otherGestureRecognizer would block the other gesture recognizer from recognizing its gesture. Note that returning YES is guaranteed to allow simultaneous recognition; returning NO, on the other hand, is not guaranteed to prevent simultaneous recognition because the other gesture recognizer's delegate may return YES.

這個方法主要是為了詢問手勢的代理拓萌,是否允許兩個手勢識別器同時識別該手勢。返回YES可以確保允許同時識別手勢司志,返回NO的話不能保證一定不能同時識別,因為其他手勢的代理也有可能返回YES骂远。

在上面demo的ViewController中遵循UIGestureRecognizerDelegate協(xié)議,設(shè)置第一個手勢的代理為self激才,添加如下代碼:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(nonnull UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

滑動黃色視圖拓型,輸出以下結(jié)果:


45EB01B5-9148-4B7A-B53B-D3FAA63BFD7B.png

可以看到兩個手勢的action同時被調(diào)用了。

UIControl

UIControl是系統(tǒng)提供的能夠以target-action模式處理觸摸事件的控件压固,iOS中UIButton靠闭、UISegmentControl、UISwitch等控件都是UIControl的子類谣光。值得注意的是萄金,UIControl是UIView的子類媚朦,因此本身也具有UIResponder的屬性莲镣。UIControl是一個抽象基類瑞侮,我們不能直接使用UIControl類來實例化控件的圆,它只是為控件子類定義一些通用的接口越妈,并提供一些基礎(chǔ)實現(xiàn)梅掠。

UIControl作為能夠響應(yīng)事件的控件阎抒,必然也需要待事件交互符合條件時才去響應(yīng)且叁,因此也會跟蹤事件發(fā)生的過程逞带,不同于UIControl以及UIGestureRecognizer通過touches系列方法跟蹤展氓,UIControl有其獨特的跟蹤方式:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;

乍一看遇汞,這四個方法和UIResponder那四個方法幾乎吻合勺疼,只不過UIControl只能接收單點觸控执庐,因此這四個方法的參數(shù)是單個的UITouch對象轨淌。這四個方法的智能也和UIResponder一致递鹉,用來跟蹤觸摸的開始躏结、滑動媳拴、結(jié)束屈溉、取消子巾。不過线梗,UIControl本身也是UIResponder仪搔,因此同樣有touches系列的4個方法僻造。事實上髓削,UIControl的Tracking系列方法是在touch系列方法內(nèi)部調(diào)用的立膛。比如beginTrackingWithTouch是在touchesBegan方法內(nèi)部調(diào)用的宝泵。這個我們也可以驗證:

自定義一個繼承于UIControl的子類儿奶,重寫beginTrackingWithTouch和touchesBegan:withEvent:方法椰弊。
代碼如下:

@implementation Control

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"進(jìn)入%s", __func__);
    [super touchesBegan:touches withEvent:event];
    NSLog(@"離開%s", __func__);
}

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
    NSLog(@"%s", __func__);
    return [super beginTrackingWithTouch:touch withEvent:event];
}

@end

點擊自定義的Control秉版,輸出以下結(jié)果:


{2BE316C1-94A7-4C85-8CCD-68FBEB8170CE}.png

可以看出清焕,touchesBegan方法調(diào)用了beginTrackingWithTouch方法秸妥。這也說明了另外一個問題筛峭,UIControl的touchesBegan方法的實現(xiàn)與UIResponder的touchesBegan方法是有區(qū)別的影晓。

當(dāng)UIControl跟蹤事件的過程中,識別出事件交互符合響應(yīng)條件盼产,就會觸發(fā)target-action進(jìn)行響應(yīng)侨核。UIControl控件通過addTarget:action:forControlEvents:添加事件處理的target和action搓译,當(dāng)事件發(fā)生時些己,UIControl會調(diào)用sendAction:to:forEvent:來將event發(fā)送給UIApplication對象段标,再由UIApplication對象調(diào)用其sendAction:to:fromSender:forEvent:方法來將消息分發(fā)到指定的target上逼庞。

C8509D7E-767F-45F8-9F4F-2012288348AA.png

因此械荷,我們可以通過重寫UIControl的sendAction:to:forEvent:或sendAction:to:from:forEvent:自定義事件執(zhí)行的target及action。

If you specify nil for the target object, the control searches the responder chain for an object that defines the specified action method.

另外穆咐,若不指定target对湃,即addTarget:action:forControlEvents:時target傳空拍柒,那么當(dāng)事件發(fā)生時拆讯,UIControl會在響應(yīng)鏈從上往下尋找定義了指定action方法的對象來響應(yīng)該action种呐。

UIControl里面還有一個方法叫做sendActionsForControlEvents:這個方法的作用是發(fā)送與指定類型相關(guān)的所有行為消息。我們可以在任意位置(包括控件內(nèi)部和外部)調(diào)用控件的這個方法來發(fā)送參數(shù)controlEvents指定的消息阔墩。

UIControl啸箫、UIResponder筐高、UIGestureRecognizer之間的優(yōu)先級關(guān)系

上面我們已經(jīng)分析過了柑土,UIGestureRecognizer的優(yōu)先級是比UIResponder的優(yōu)先級高的稽屏,那么如果再加上一個UIControl呢坛增?
我們先來比較一下UIControl和UIResponder之間的優(yōu)先級關(guān)系收捣,這里的UIResponder我們用UIView來代替

首先如果UIControl添加在UIView上面的時候罢艾,毋庸置疑咐蚯,UIControl會首先響應(yīng)春锋,參照button添加在視圖上期奔。那么如果把UIView添加在UIControl上面的時候,誰會響應(yīng)事件呢搁胆?我們用代碼來驗證一下:

自定義一個UIView渠旁,重寫touchesBegan:方法

@implementation YellowView

- (void)touchesBegan:(NSSet<UITouch *> *)touches   withEvent:(UIEvent *)event
{
    NSLog(@"%s", __func__);
}

@end

自定義一個UIControl顾腊,里面什么都不用寫梆惯。

在ViewController里面添加如下代碼:

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Control *control = [[Control alloc] initWithFrame:CGRectMake(100, 100, 100, 200)];
    control.backgroundColor = [UIColor redColor];
    [control addTarget:self action:@selector(clickControl:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:control];

    YellowView *yellowView = [[YellowView alloc] initWithFrame:CGRectMake(10, 10, 50, 50)];
    yellowView.backgroundColor = [UIColor yellowColor];
    [control addSubview:yellowView];
}

- (void)clickControl:(UIControl *)control
{
    NSLog(@"click Control");
}

@end

點擊上層的UIControl垛吗,輸出結(jié)果如下:


E0B59E0B-0468-46E4-91F4-0B723E2BDD95.png

可以看到UIControl的action并沒有響應(yīng)∠锹纾看來自定義的UIControl與UIResponder之間的優(yōu)先級還是遵循響應(yīng)鏈的層級的羡儿,這就表示UIResponder和UIControl的優(yōu)先級是相同的,而UIGestureRecognizer的優(yōu)先級比UIControl高,由此推斷的話码泞,UIGestureRecognizer的優(yōu)先級好像是比UIControl高的余寥,具體是什么樣子的宋舷,我們還是來驗證一下。

現(xiàn)在我們把層級改變一下绎狭,把UIControl添加到y(tǒng)ellowView上面儡嘶,然后給yellowView添加一個tap手勢蹦狂,代碼如下:

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    YellowView *blueView = [[YellowView alloc] initWithFrame:CGRectMake(100, 100, 100, 200)];
    blueView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:blueView];

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
    [blueView addGestureRecognizer:tap];

    Control *control = [[Control alloc] initWithFrame:CGRectMake(10, 10, 50, 50)];
    control.backgroundColor = [UIColor redColor];
    [control addTarget:self action:@selector(clickControl:) forControlEvents:UIControlEventTouchUpInside];
    [blueView addSubview:control];
}

- (void)clickControl:(UIControl *)control
{
    NSLog(@"click Control");
}

- (void)tap:(UIGestureRecognizer *)gestureRecognizer
{
    NSLog(@"tap");
}

@end

這里為了讓我們的代碼更嚴(yán)謹(jǐn)窜骄,我們把上面YellowView中重寫的touchesBegan方法注掉“⊙校現(xiàn)在點擊自定義的Control,打印結(jié)果如下:

1A500D7E-69C1-4BA4-B537-660B4D4F90D1.png

可以看到沟娱,系統(tǒng)執(zhí)行了手勢的action,并沒有執(zhí)行UIControl的action砰蠢,這好像跟我們上面預(yù)測的手勢的優(yōu)先級比UIControl高是一致的台舱。但是真的是這樣嗎竞惋?我們把自定義的UIControl替換成UIButton,其它地方不變浑厚,再點擊一次button瞻颂,打印結(jié)果變成了這樣:


A4E97A1F-19D8-4B80-BCEB-625FC3EA2D1F.png

同樣都是繼承于UIControl,這control和control的差別咋就那么大捏盖矫?责掏?换衬?

別急瞳浦,蘋果爸爸已經(jīng)給了我們合理的解釋:

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.

在iOS6以后,默認(rèn)的control actions會阻止與該action操作相同的手勢的識別矗蕊。例如:UIButton的默認(rèn)操作是單擊,如果你在這個button的父視圖上面添加了一個tap手勢没龙,用戶單擊button,系統(tǒng)會調(diào)用button的action而不是手勢的action赃磨。這種規(guī)則僅僅適用于手勢操作和UIcontrol的默認(rèn)操作相同的情況下,包含以下幾種情況:

  • 單擊:UIButton值骇,UISwitch,UIStepper使碾,UISegmentedControl和UIPageControl
  • 滑動:UISlider票摇,滑動方向與slider平行
  • 拖動:UISwitch盆色,拖動方向與switch平行

這里提到了兩點隔躲,第一是手勢和UIControl的默認(rèn)操作相同,也就是說如果UIControl沒有默認(rèn)操作(比如我們自定義的UIControl)或者是默認(rèn)操作和添加的手勢不同响鹃,那么手勢識別器的識別優(yōu)先級高买置,UIControl不會阻止手勢識別。第二是在UIButton的父視圖上添加手勢轩触,如果你把一個添加了手勢的視圖添加在UIButton上面脱柱,那么UIButton是不能阻止該手勢識別的。這兩點讀者可以自行驗證随闺。

總結(jié):自定義的UIControl和UIResponder的優(yōu)先級相同矩乐,都比UIGestureRecognizer低,有默認(rèn)操作的UIControl會組織添加在父視圖上面的有相同操作的手勢的識別回论。

參考文章:
iOS觸摸事件全家桶
手勢識別(四)多手勢間的交互與共存
iOS觸摸事件傳遞響應(yīng)之被忽視的手勢識別器工作原理
iOS開發(fā)系列--觸摸事件绰精、手勢識別撒璧、搖晃事件、耳機(jī)線控
UIKit: UIControl
iOS觸摸事件的流動

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末笨使,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子硫椰,更是在濱河造成了極大的恐慌繁调,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件靶草,死亡現(xiàn)場離奇詭異蹄胰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)奕翔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門裕寨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人派继,你說我怎么就攤上這事宾袜。” “怎么了驾窟?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵庆猫,是天一觀的道長。 經(jīng)常有香客問我绅络,道長月培,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任恩急,我火速辦了婚禮杉畜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘衷恭。我一直安慰自己此叠,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布匾荆。 她就那樣靜靜地躺著,像睡著了一般杆烁。 火紅的嫁衣襯著肌膚如雪牙丽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天兔魂,我揣著相機(jī)與錄音烤芦,去河邊找鬼。 笑死析校,一個胖子當(dāng)著我的面吹牛构罗,可吹牛的內(nèi)容都是我干的铜涉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼遂唧,長吁一口氣:“原來是場噩夢啊……” “哼芙代!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起盖彭,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤纹烹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后召边,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铺呵,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年隧熙,在試婚紗的時候發(fā)現(xiàn)自己被綠了片挂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡贞盯,死狀恐怖音念,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情邻悬,我是刑警寧澤症昏,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站父丰,受9級特大地震影響肝谭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蛾扇,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一攘烛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧镀首,春花似錦坟漱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怜校。三九已至样刷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間想暗,已是汗流浹背麻敌。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工栅炒, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓赢赊,卻偏偏與公主長得像乙漓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子释移,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的叭披?困惑于Cell怎么突然不能點擊了?糾結(jié)于如何實現(xiàn)這個奇葩響應(yīng)需求秀鞭?亦或是...
    Lotheve閱讀 56,660評論 51 597
  • 在iOS開發(fā)中經(jīng)常會涉及到觸摸事件趋观。本想自己總結(jié)一下,但是遇到了這篇文章锋边,感覺總結(jié)的已經(jīng)很到位皱坛,特此轉(zhuǎn)載。作者:L...
    WQ_UESTC閱讀 5,988評論 4 26
  • 簡介 iOS 事件分為三大類 觸摸事件 加速器事件 遠(yuǎn)程控制事件 以下我們講解觸摸事件觸摸事件是我們平時遇到最多的...
    AKsoftware閱讀 22,411評論 23 72
  • iOS中的事件 用戶與app交互的時候會產(chǎn)生各種各樣的的事件豆巨,iOS中事件分為三大類型:1)觸摸事件 剩辟;2)加速計...
    jason_Yun閱讀 706評論 0 3
  • -- iOS事件全面解析 概覽 iPhone的成功很大一部分得益于它多點觸摸的強(qiáng)大功能,喬布斯讓人們認(rèn)識到手機(jī)其實...
    翹楚iOS9閱讀 2,942評論 0 13