咱們的App與用戶進行交互,基本上是依賴於各類各樣的事件。例如,用戶點擊界面上的按鈕,咱們須要觸發一個按鈕點擊事件,並進行相應的處理,以給用戶一個響應。UIView的三大職責之一就是處理事件,一個視圖是一個事件響應者,能夠處理點擊等事件,而這些事件就是在UIResponder類中定義的。css
一個UIResponder類爲那些須要響應並處理事件的對象定義了一組接口。這些事件主要分爲兩類:觸摸事件(touch events)和運動事件(motion events)。UIResponder類爲每兩類事件都定義了一組接口,這個咱們將在下面詳細描述。html
在UIKit中,UIApplication、UIView、UIViewController這幾個類都是直接繼承自UIResponder類。另外SpriteKit中的SKNode也是繼承自UIResponder類。所以UIKit中的視圖、控件、視圖控制器,以及咱們自定義的視圖及視圖控制器都有響應事件的能力。這些對象一般被稱爲響應對象,或者是響應者(如下咱們統一使用響應者)。ios
本文將詳細介紹一個UIResponder類提供的基本功能。不過在此以前,咱們先來了解一下事件響應鏈機制。數組
大多數事件的分發都是依賴響應鏈的。響應鏈是由一系列連接在一塊兒的響應者組成的。通常狀況下,一條響應鏈開始於第一響應者,結束於application對象。若是一個響應者不能處理事件,則會將事件沿着響應鏈傳到下一響應者。ruby
那這裏就會有三個問題:架構
響應鏈是什麼時候構建的app
系統是如何肯定第一響應者的ide
肯定第一響應者後,系統又是按照什麼樣的順序來傳遞事件的動畫
咱們都知道在一個App中,全部視圖是按必定的結構組織起來的,即樹狀層次結構。除了根視圖外,每一個視圖都有一個父視圖;而每一個視圖均可以有0個或多個子視圖。而在這個樹狀結構構建的同時,也構建了一條條的事件響應鏈。atom
當用戶觸發某一事件(觸摸事件或運動事件)後,UIKit會建立一個事件對象(UIEvent),該對象包含一些處理事件所須要的信息。而後事件對象被放到一個事件隊列中。這些事件按照先進先出的順序來處理。當處理事件時,程序的UIApplication對象會從隊列頭部取出一個事件對象,將其分發出去。一般首先是將事件分發給程序的主window對象,對於觸摸事件來說,window對象會首先嚐試將事件分發給觸摸事件發生的那個視圖上。這一視圖一般被稱爲hit-test視圖,而查找這一視圖的過程就叫作hit-testing。
系統使用hit-testing來找到觸摸下的視圖,它檢測一個觸摸事件是否發生在相應視圖對象的邊界以內(即視圖的frame屬性,這也是爲何子視圖若是在父視圖的frame以外時,是沒法響應事件的)。若是在,則會遞歸檢測其全部的子視圖。包含觸摸點的視圖層次架構中最底層的視圖就是hit-test視圖。在檢測出hit-test視圖後,系統就將事件發送給這個視圖來進行處理。
咱們經過一個示例來演示hit-testing的過程。圖1是一個視圖層次結構,
假設用戶點擊了視圖E,系統按照如下順序來查找hit-test視圖:
點擊事件發生在視圖A的邊界內,因此檢測子視圖B和C;
點擊事件不在視圖B的邊界內,但在視圖C的邊界範圍內,因此檢測子圖片D和E;
點擊事件不在視圖D的邊界內,但在視圖E的邊界範圍內;
視圖E是包含觸摸點的視圖層次架構中最底層的視圖(倒樹結構),因此它就是hit-test視圖。
hit-test視圖能夠最早去處理觸摸事件,若是hit-test視圖不能處理事件,則事件會沿着響應鏈往上傳遞,直到找到能處理它的視圖。
最有機會處理事件的對象是hit-test視圖或第一響應者。若是這二者都不能處理事件,UIKit就會將事件傳遞到響應鏈中的下一個響應者。每個響應者肯定其是否要處理事件或者是經過nextResponder方法將其傳遞給下一個響應者。這一過程一直持續到找到能處理事件的響應者對象或者最終沒有找到響應者。
圖2演示了這樣一個事件傳遞的流程,
當系統檢測到一個事件時,將其傳遞給初始對象,這個對象一般是一個視圖。而後,會按如下路徑來處理事件(咱們以左圖爲例):
初始視圖(initial view)嘗試處理事件。若是它不能處理事件,則將事件傳遞給其父視圖。
初始視圖的父視圖(superview)嘗試處理事件。若是這個父視圖還不能處理事件,則繼續將視圖傳遞給上層視圖。
上層視圖(topmost view)會嘗試處理事件。若是這個上層視圖仍是不能處理事件,則將事件傳遞給視圖所在的視圖控制器。
視圖控制器會嘗試處理事件。若是這個視圖控制器不能處理事件,則將事件傳遞給窗口(window)對象。
窗口(window)對象嘗試處理事件。若是不能處理,則將事件傳遞給單例app對象。
若是app對象不能處理事件,則丟棄這個事件。
從上面能夠看到,視圖、視圖控制器、窗口對象和app對象都能處理事件。另外須要注意的是,手勢也會影響到事件的傳遞。
以上即是響應鏈的一些基本知識。有了這些知識,咱們即可以來看看UIResponder提供給咱們的一些方法了。
UIResponder提供了幾個方法來管理響應鏈,包括讓響應對象成爲第一響應者、放棄第一響應者、檢測是不是第一響應者以及傳遞事件到下一響應者的方法,咱們分別來介紹一下。
上面提到在響應鏈中負責傳遞事件的方法是nextResponder,其聲明以下:
- (UIResponder *)nextResponder
UIResponder類並不自動保存或設置下一個響應者,該方法的默認實現是返回nil。子類的實現必須重寫這個方法來設置下一響應者。UIView的實現是返回管理它的UIViewController對象(若是它有)或者其父視圖。而UIViewController的實現是返回它的視圖的父視圖;UIWindow的實現是返回app對象;而UIApplication的實現是返回nil。因此,響應鏈是在構建視圖層次結構時生成的。
一個響應對象能夠成爲第一響應者,也能夠放棄第一響應者。爲此,UIResponder提供了一系列方法,咱們分別來介紹一下。
若是想斷定一個響應對象是不是第一響應者,則可使用如下方法:
- (BOOL)isFirstResponder
若是咱們但願將一個響應對象做爲第一響應者,則可使用如下方法:
- (BOOL)becomeFirstResponder
若是對象成爲第一響應者,則返回YES;不然返回NO。默認實現是返回YES。子類能夠重寫這個方法來更新狀態,或者來執行一些其它的行爲。
一個響應對象只有在當前響應者能放棄第一響應者狀態(canResignFirstResponder)且自身能成爲第一響應者(canBecomeFirstResponder)時纔會成爲第一響應者。
這個方法相信你們用得比較多,特別是在但願UITextField獲取焦點時。另外須要注意的是隻有當視圖是視圖層次結構的一部分時才調用這個方法。若是視圖的window屬性不爲空時,視圖纔在一個視圖層次結構中;若是該屬性爲nil,則視圖不在任何層次結構中。
上面提到一個響應對象成爲第一響應者的一個前提是它能夠成爲第一響應者,咱們可使用canBecomeFirstResponder方法來檢測,
- (BOOL)canBecomeFirstResponder
須要注意的是咱們不能向一個不在視圖層次結構中的視圖發送這個消息,其結果是未定義的。
與上面兩個方法相對應的是響應者放棄第一響應者的方法,其定義以下:
- (BOOL)resignFirstResponder- (BOOL)canResignFirstResponder
resignFirstResponder默認也是返回YES。須要注意的是,若是子類要重寫這個方法,則在咱們的代碼中必須調用super的實現。
canResignFirstResponder默認也是返回YES。不過有些狀況下可能須要返回NO,如一個輸入框在輸入過程當中可能須要讓這個方法返回NO,以確保在編輯過程當中能始終保證是第一響應者。
所謂的輸入視圖,是指當對象爲第一響應者時,顯示另一個視圖用來處理當前對象的信息輸入,如UITextView和UITextField兩個對象,在其成爲第一響應者是,會顯示一個系統鍵盤,用來輸入信息。這個系統鍵盤就是輸入視圖。輸入視圖有兩種,一個是inputView,另外一個是inputAccessoryView。這二者如圖3所示:
與inputView相關的屬性有以下兩個,
@property(nonatomic, readonly, retain) UIView *inputView@property(nonatomic, readonly, retain) UIInputViewController *inputViewController
這兩個屬性提供一個視圖(或視圖控制器)用於替代爲UITextField和UITextView彈出的系統鍵盤。咱們能夠在子類中將這兩個屬性從新定義爲讀寫屬性來設置這個屬性。若是咱們須要本身寫一個鍵盤的,如爲輸入框定義一個用於輸入身份證的鍵盤(只包含0-9和X),則可使用這兩個屬性來獲取這個鍵盤。
與inputView相似,inputAccessoryView也有兩個相關的屬性:
@property(nonatomic, readonly, retain) UIView *inputAccessoryView@property(nonatomic, readonly, retain) UIInputViewController *inputAccessoryViewController
設置方法與前面相同,都是在子類中從新定義爲可讀寫屬性,以設置這個屬性。
另外,UIResponder還提供瞭如下方法,在對象是第一響應者時更新輸入和訪問視圖,
- (void)reloadInputViews
調用這個方法時,視圖會當即被替換,即不會有動畫之類的過渡。若是當前對象不是第一響應者,則該方法是無效的。
UIResponder提供了以下四個你們都很是熟悉的方法來響應觸摸事件:
// 當一個或多個手指觸摸到一個視圖或窗口- (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
這四個方法默認都是什麼都不作。不過,UIKit中UIResponder的子類,尤爲是UIView,這幾個方法的實現都會把消息傳遞到響應鏈上。所以,爲了避免阻斷響應鏈,咱們的子類在重寫時須要調用父類的相應方法;而不要將消息直接發送給下一響應者。
默認狀況下,多點觸摸是被禁用的。爲了接受多點觸摸事件,咱們須要設置響應視圖的multipleTouchEnabled屬性爲YES。
與觸摸事件相似,UIResponder也提供了幾個方法來響應移動事件:
// 移動事件開始- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event// 移動事件結束- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event// 取消移動事件- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
與觸摸事件不一樣的是,運動事件只有開始與結束操做;它不會報告相似於晃動這樣的事件。這幾個方法的默認操做也是什麼都不作。不過,UIKit中UIResponder的子類,尤爲是UIView,這幾個方法的實現都會把消息傳遞到響應鏈上。
遠程控制事件來源於一些外部的配件,如耳機等。用戶能夠經過耳機來控制視頻或音頻的播放。接收響應者對象須要檢查事件的子類型來肯定命令(如播放,子類型爲UIEventSubtypeRemoteControlPlay),而後進行相應處理。
爲了響應遠程控制事件,UIResponder提供瞭如下方法,
- (void)remoteControlReceivedWithEvent:(UIEvent *)event
咱們能夠在子類中實現該方法,來處理遠程控制事件。不過,爲了容許分發遠程控制事件,咱們必須調用UIApplication的beginReceivingRemoteControlEvents方法;而若是要關閉遠程控制事件的分發,則調用endReceivingRemoteControlEvents方法。
默認狀況下,程序的每個window都有一個undo管理器,它是一個用於管理undo和redo操做的共享對象。然而,響應鏈上的任何對象的類均可以有自定義undo管理器。例如,UITextField的實例的自定義管理器在文件輸入框放棄第一響應者狀態時會被清理掉。當須要一個undo管理器時,請求會沿着響應鏈傳遞,而後UIWindow對象會返回一個可用的實例。
UIResponder提供了一個只讀方法來獲取響應鏈中共享的undo管理器,
@property(nonatomic, readonly) NSUndoManager *undoManager
咱們能夠在本身的視圖控制器中添加undo管理器來執行其對應的視圖的undo和redo操做。
在咱們的應用中,常常會處理各類菜單命令,如文本輸入框的」複製」、」粘貼」等。UIResponder爲此提供了兩個方法來支持此類操做。首先使用如下方法能夠啓動或禁用指定的命令:
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
該方法默認返回YES,咱們的類能夠經過某種途徑處理這個命令,包括類自己或者其下一個響應者。子類能夠重寫這個方法來開啓菜單命令。例如,若是咱們但願菜單支持」Copy」而不支持」Paser」,則在咱們的子類中實現該方法。須要注意的是,即便在子類中禁用某個命令,在響應鏈上的其它響應者也可能會處理這些命令。
另外,咱們可使用如下方法來獲取能夠響應某一行爲的接收者:
- (id)targetForAction:(SEL)action withSender:(id)sender
在對象須要調用一個action操做時調用該方法。默認的實現是調用canPerformAction:withSender:方法來肯定對象是否能夠調用action操做。若是能夠,則返回對象自己,不然將請求傳遞到響應鏈上。若是咱們想要重寫目標的選擇方式,則應該重寫這個方法。下面這段代碼演示了一個文本輸入域禁用拷貝/粘貼操做:
- (id)targetForAction:(SEL)action withSender:(id)sender { UIMenuController *menuController = [UIMenuController sharedMenuController]; if (action == @selector(selectAll:) || action == @selector(paste:) ||action == @selector(copy:) || action == @selector(cut:)) { if (menuController) { [UIMenuController sharedMenuController].menuVisible = NO; } return nil; } return [super targetForAction:action withSender:sender]; }
咱們的應用能夠支持外部設備,包括外部鍵盤。在使用外部鍵盤時,使用快捷鍵能夠大大提升咱們的輸入效率。所以從iOS7後,UIResponder類新增了一個只讀屬性keyCommands,來定義一個響應者支持的快捷鍵,其聲明以下:
@property(nonatomic, readonly) NSArray *keyCommands
一個支持硬件鍵盤命令的響應者對象能夠從新定義這個方法並使用它來返回一個其所支持快捷鍵對象(UIKeyCommand)的數組。每個快捷鍵命令表示識別的鍵盤序列及響應者的操做方法。
咱們用這個方法返回的快捷鍵命令數組被用於整個響應鏈。當與快捷鍵命令對象匹配的快捷鍵被按下時,UIKit會沿着響應鏈查找實現了響應行爲方法的對象。它調用找到的第一個對象的方法並中止事件的處理。
文本輸入模式標識當響應者激活時的語言及顯示的鍵盤。UIResponder爲此定義了一個屬性來返回響應者對象的文本輸入模式:
@property(nonatomic, readonly, retain) UITextInputMode *textInputMode
對於響應者而言,系統一般顯示一個基於用戶語言設置的鍵盤。咱們能夠從新定義這個屬性,並讓它返回一個不一樣的文本輸入模式,以讓咱們的響應者使用一個特定的鍵盤。用戶在響應者被激活時仍然能夠改變鍵盤,在切換到另外一個響應者時,能夠再恢復到指定的鍵盤。
若是咱們想讓UIKit來跟蹤這個響應者的文本輸入模式,咱們能夠經過textInputContextIdentifier屬性來設置一個標識,該屬性的聲明以下:
@property(nonatomic, readonly, retain) NSString *textInputContextIdentifier
該標識指明響應者應保留文本輸入模式的信息。在跟蹤模式下,任何對文本輸入模式的修改都會記錄下來,當響應者激活時再用於恢復處理。
爲了從程序的user default中清理輸入模式信息,UIResponder定義了一個類方法,其聲明以下:
+ (void)clearTextInputContextIdentifier:(NSString *)identifier
調用這個方法能夠從程序的user default中移除與指定標識相關的全部文本輸入模式。移除這些信息會讓響應者從新使用默認的文本輸入模式。
從iOS 8起,蘋果爲咱們提供了一個很是棒的功能,即Handoff。使用這一功能,咱們能夠在一部iOS設備的某個應用上開始作一件事,而後在另外一臺iOS設備上繼續作這件事。Handoff的基本思想是用戶在一個應用裏所作的任何操做均可以看做是一個Activity,一個Activity能夠和一個特定iCloud用戶的多臺設備關聯起來。在編寫一個支持Handoff的應用時,會有如下三個交互事件:
爲將在另外一臺設備上繼續作的事建立一個新的User Activity;
當須要時,用新的數據更新已有的User Activity;
把一個User Activity傳遞到另外一臺設備上。
爲了支持這些交互事件,在iOS 8後,UIResponder類新增了幾個方法,咱們在此不討論這幾個方法的實際使用,想了解更多的話,能夠參考 iOS 8 Handoff 開發指南 。咱們在此只是簡單描述一下這幾個方法。
在UIResponder中,已經爲咱們提供了一個userActivity屬性,它是一個NSUserActivity對象。所以咱們在UIResponder的子類中不須要再去聲明一個userActivity屬性,直接使用它就行。其聲明以下:
@property(nonatomic, retain) NSUserActivity *userActivity
由UIKit管理的User Activities會在適當的時間自動保存。通常狀況下,咱們能夠重寫UIResponder類的updateUserActivityState:方法來延遲添加表示User Activity的狀態數據。當咱們再也不須要一個User Activity時,咱們能夠設置userActivity屬性爲nil。任何由UIKit管理的NSUserActivity對象,若是它沒有相關的響應者,則會自動失效。
另外,多個響應者能夠共享一個NSUserActivity實例。
上面提到的updateUserActivityState:是用於更新給定的User Activity的狀態。其定義以下:
- (void)updateUserActivityState:(NSUserActivity *)activity
子類能夠重寫這個方法來按照咱們的須要更新給定的User Activity。咱們須要使用NSUserActivity對象的addUserInfoEntriesFromDictionary:方法來添加表示用戶Activity的狀態。
在咱們修改了User Activity的狀態後,若是想將其恢復到某個狀態,則可使用如下方法:
- (void)restoreUserActivityState:(NSUserActivity *)activity
子類能夠重寫這個方法來使用給定User Activity的恢復響應者的狀態。系統會在接收到數據時,將數據傳遞給application:continueUserActivity:restorationHandler:以作處理。咱們重寫時應該使用存儲在user activity的userInfo字典中的狀態數據來恢復對象。固然,咱們也能夠直接調用這個方法。