ios學習之UIKit框架(1)--UIResponder

轉載自  http://www.tuicool.com/articles/E3UBJj3


我們的App與用戶進行交互,基本上是依賴於各種各樣的事件。例如,用戶點擊界面上的按鈕,我們需要觸發一個按鈕點擊事件,並進行相應的處理,以給用戶一個響應。UIView的三大職責之一就是處理事件,一個視圖是一個事件響應者,可以處理點擊等事件,而這些事件就是在UIResponder類中定義的。

一個UIResponder類爲那些需要響應並處理事件的對象定義了一組接口。這些事件主要分爲兩類:觸摸事件(touch events)和運動事件(motion events)。UIResponder類爲每兩類事件都定義了一組接口,這個我們將在下面詳細描述。

在UIKit中,UIApplication、UIView、UIViewController這幾個類都是直接繼承自UIResponder類。另外SpriteKit中的SKNode也是繼承自UIResponder類。因此UIKit中的視圖、控件、視圖控制器,以及我們自定義的視圖及視圖控制器都有響應事件的能力。這些對象通常被稱爲響應對象,或者是響應者(以下我們統一使用響應者)。

本文將詳細介紹一個UIResponder類提供的基本功能。不過在此之前,我們先來了解一下事件響應鏈機制。

響應鏈

大多數事件的分發都是依賴響應鏈的。響應鏈是由一系列鏈接在一起的響應者組成的。一般情況下,一條響應鏈開始於第一響應者,結束於application對象。如果一個響應者不能處理事件,則會將事件沿着響應鏈傳到下一響應者。

那這裏就會有三個問題:

  1. 響應鏈是何時構建的

  2. 系統是如何確定第一響應者的

  3. 確定第一響應者後,系統又是按照什麼樣的順序來傳遞事件的

構建響應鏈

我們都知道在一個App中,所有視圖是按一定的結構組織起來的,即樹狀層次結構。除了根視圖外,每個視圖都有一個父視圖;而每個視圖都可以有0個或多個子視圖。而在這個樹狀結構構建的同時,也構建了一條條的事件響應鏈。

確定第一響應者

當用戶觸發某一事件(觸摸事件或運動事件)後,UIKit會創建一個事件對象(UIEvent),該對象包含一些處理事件所需要的信息。然後事件對象被放到一個事件隊列中。這些事件按照先進先出的順序來處理。當處理事件時,程序的UIApplication對象會從隊列頭部取出一個事件對象,將其分發出去。通常首先是將事件分發給程序的主window對象,對於觸摸事件來講,window對象會首先嚐試將事件分發給觸摸事件發生的那個視圖上。這一視圖通常被稱爲hit-test視圖,而查找這一視圖的過程就叫做hit-testing。

系統使用hit-testing來找到觸摸下的視圖,它檢測一個觸摸事件是否發生在相應視圖對象的邊界之內(即視圖的frame屬性,這也是爲什麼子視圖如果在父視圖的frame之外時,是無法響應事件的)。如果在,則會遞歸檢測其所有的子視圖。包含觸摸點的視圖層次架構中最底層的視圖就是hit-test視圖。在檢測出hit-test視圖後,系統就將事件發送給這個視圖來進行處理。

我們通過一個示例來演示hit-testing的過程。圖1是一個視圖層次結構,

RVVZre.png

假設用戶點擊了視圖E,系統按照以下順序來查找hit-test視圖:

  1. 點擊事件發生在視圖A的邊界內,所以檢測子視圖B和C;

  2. 點擊事件不在視圖B的邊界內,但在視圖C的邊界範圍內,所以檢測子圖片D和E;

  3. 點擊事件不在視圖D的邊界內,但在視圖E的邊界範圍內;

視圖E是包含觸摸點的視圖層次架構中最底層的視圖(倒樹結構),所以它就是hit-test視圖。

hit-test視圖可以最先去處理觸摸事件,如果hit-test視圖不能處理事件,則事件會沿着響應鏈往上傳遞,直到找到能處理它的視圖。

事件傳遞

最有機會處理事件的對象是hit-test視圖或第一響應者。如果這兩者都不能處理事件,UIKit就會將事件傳遞到響應鏈中的下一個響應者。每一個響應者確定其是否要處理事件或者是通過nextResponder方法將其傳遞給下一個響應者。這一過程一直持續到找到能處理事件的響應者對象或者最終沒有找到響應者。

圖2演示了這樣一個事件傳遞的流程,

MjAN7ry.png

當系統檢測到一個事件時,將其傳遞給初始對象,這個對象通常是一個視圖。然後,會按以下路徑來處理事件(我們以左圖爲例):

  1. 初始視圖(initial view)嘗試處理事件。如果它不能處理事件,則將事件傳遞給其父視圖。

  2. 初始視圖的父視圖(superview)嘗試處理事件。如果這個父視圖還不能處理事件,則繼續將視圖傳遞給上層視圖。

  3. 上層視圖(topmost view)會嘗試處理事件。如果這個上層視圖還是不能處理事件,則將事件傳遞給視圖所在的視圖控制器。

  4. 視圖控制器會嘗試處理事件。如果這個視圖控制器不能處理事件,則將事件傳遞給窗口(window)對象。

  5. 窗口(window)對象嘗試處理事件。如果不能處理,則將事件傳遞給單例app對象。

  6. 如果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所示:

aAz6Vb.jpg

與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方法。

獲取Undo管理器

默認情況下,程序的每一個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中移除與指定標識相關的所有文本輸入模式。移除這些信息會讓響應者重新使用默認的文本輸入模式。

支持User Activities

從iOS 8起,蘋果爲我們提供了一個非常棒的功能,即Handoff。使用這一功能,我們可以在一部iOS設備的某個應用上開始做一件事,然後在另一臺iOS設備上繼續做這件事。Handoff的基本思想是用戶在一個應用裏所做的任何操作都可以看作是一個Activity,一個Activity可以和一個特定iCloud用戶的多臺設備關聯起來。在編寫一個支持Handoff的應用時,會有以下三個交互事件:

  1. 爲將在另一臺設備上繼續做的事創建一個新的User Activity;

  2. 當需要時,用新的數據更新已有的User Activity;

  3. 把一個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字典中的狀態數據來恢復對象。當然,我們也可以直接調用這個方法。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章