iOS概念攻堅之路(六):事件傳遞與響應

前言

這篇文章主要想弄清楚事件(如觸摸屏幕)產生後,系統是如何通知到你的 App,在 App 內部是如何進行傳遞,最終又是如何肯定最終的響應者的。html

這些確定是有規則的,在 App 內部,一個事件會按照一個規則(視圖層級關係)去遍歷尋找這個事件的最佳響應者,可是這個響應者有可能不處理事件,那麼它又須要沿着必定的規則(響應者鏈)去傳遞這個事件,若是最終都無人處理,那麼將這個事件拋棄,也就是不處理。ios

事件

先來看看什麼是事件。git

事件對應的對象爲 UIEvent,它有一個屬性爲 type,是 EventType 類型,EventType 是一個枚舉類型:github

public enum EventType : Int {
    case touches    // 觸摸事件
    case motion     // 運動事件
    case remoteControl  // 遠程控制事件
    @available(iOS 9.0, *)
    case presses    // 按壓事件
}
複製代碼

因此 iOS 中的事件有四種:面試

  • touch events(觸摸事件)
  • motion events(運動事件)
  • remote-control events(遠程控制事件)
  • press events(按壓事件)

1.觸摸事件

觸摸事件就是咱們的手指或者蘋果的 Pencil(觸筆)在屏幕中所引起的互動,好比輕點、長按、滑動等操做,是咱們最常接觸到的事件類型。觸摸事件對象能夠包含一個或多個觸摸,而且每一個觸摸由 UITouch 對象表示。當觸摸事件發生時,系統會將其沿着線路傳遞,找到適當的響應者並調用適當的方法,例如 touchedBegan:withEvent:。響應者對象會根據觸摸來肯定適當的方法。算法

觸摸事件分爲如下幾類:bootstrap

  • 手勢事件
    • 長按手勢(UILongPressGestureRecognizer
    • 拖動手勢(UIPanGestureRecognizer
    • 捏合手勢(UIPinchGestureRecognizer
    • 響應屏幕邊緣手勢(UIScreenEdgePanGestureRecognizer
    • 輕掃手勢(UISwipeGestureRecognizer
    • 旋轉手勢(UIRotationGestureRecognizer
    • 點擊手勢(UITapGestureRecognizer
  • 自定義手勢
  • 點擊 button 相關

觸摸事件對應的對象爲 UITouchswift

2.運動事件

iPhone 內置陀螺儀、加速器和磁力儀,能夠感知手機的運動狀況。iOS 提供了 Core Motion 框架來處理這些運動事件。根據這些內置硬件,運動事件大體分爲三類:服務器

  • 陀螺儀相關:陀螺儀會測量設備繞 X-Y-Z 軸的自轉速率、傾斜角度等。經過 Core Motion 提供的一些 API 能夠獲取到這些數據,並進行處理;經過系統能夠經過內置陀螺儀獲取設備的朝向,以此對 App UI 作出調整
  • 加速器相關:設備能夠經過內置加速器測量設備在 X-Y-Z 軸速度的改變;Core Motion 提供了高度計(CMAltimeter)、計步器(CMPedometer)等對象,來獲取處理這些產生的數據
  • 磁力儀相關:使用磁力儀能夠獲取當前設備的磁極、方向、經緯度等數據,這些數據多用於地圖導航開發

不過官方文檔中指出,這些都是屬於 Core Motion 庫框架,Core Motion 庫中的事件直接由 Core Motion 內部進行處理,不會經過響應者鏈,因此 UIKit 框架能接收的事件暫時只包括搖一搖(EventSubtype.motionShake)。app

3.遠程控制事件

遠程控制事件容許響應者對象從外部附件或耳機接受命令,以便它能夠管理音頻和視頻。目前 iOS 僅提供咱們遠程控制音頻和視頻的權限,即對音頻實現暫停/播放、上一曲/下一曲、快進/快退操做。如下是它能識別的類型:

public enum EventSubtype : Int {
    case remoteControlPlay
    case remoteControlPause
    case remoteControlStop
    case remoteControlTogglePlayPause
    case remoteControlNextTrack
    case remoteControlPreviousTrack
    case remoteControlBeginSeekingBackward
    case remoteControlEndSeekingBackward
    case remoteControlBeginSeekingForward
    case remoteControlEndSeekingForward
}
複製代碼

4.按壓事件

iOS 9.0 以後提供了 3D Touch 事件,經過使用這個功能能夠作以下操做:

  • Quick Actions:重壓 App icon 能夠進行不少快捷操做
  • Peek and Pop:使用這個功能對文件進行預覽和其餘操做,能夠在手機自帶 「信息」 裏面試驗
  • Pressure Sensitivity:壓力響應敏感,能夠在備忘錄中選擇畫筆,按壓不一樣力度畫出來的顏色深淺不同

事件傳遞到 App 以前

咱們通常說的事件傳遞的起點在於 UIApplication 所管理的事件隊列中開始分發的時候,但事件真正的起點在於你手指觸摸到屏幕的那一刻開始(以觸摸事件爲例),那麼在觸摸屏幕到事件隊列開始分發發生了什麼?咱們就以一個觸摸事件來講明這個過程。

  1. 手指觸摸屏幕,IOKit.framework 將事件封裝成一個 IOHIDEvent 對象
  2. 將這個對象經過 mach port(IPC 進程間通訊)轉發到 Springboard
  3. Springboard 經過 mach port(IPC 進程間通訊)轉發給當前 App 的主線程
  4. 前臺 App 主線程的 RunLoop 接收到 Springboard 轉發過來的消息以後,觸發對應的 mach portSource1 回調 __IOHIDEventSystemClientQueueCallback()
  5. Source1 回調內部觸發了 Source0 的回調 __UIApplicationHandleEventQueue()
  6. Source0 回調內部,封裝 IOHIDEventUIEvent
  7. Source0 回調內部調用 UIApplication+sendEvent: 方法,將 UIEvent 傳給當前 UIWindow

IOKit.framework
是一個系統框架的集合,用來驅動一些系統事件。IOHIDEvent 中的 HID 表明 Human Interface Device,即人機交互驅動

SpringBoard
是一個應用程序,用來管理 iOS 的主屏幕,除此以外像 WindowServer(窗口服務器)bootstrapping(引導應用程序),以及在啓動時候系統的一些初始化設置都是由這個特定的應用程序負責的。它是咱們 iOS 程序中,事件的第一個接收者。它只能接受少數的事件,好比:按鍵(鎖屏/靜音等)、觸摸、加速、接近傳感器等幾種 Event,隨後使用 mach port 轉發給須要的 App 進程

UIApplication 管理了一個事件隊列,之因此是隊列而不是棧,是由於隊列的特色是先進先出,先產生的事件先處理。UIApplication 會從事件隊列中取出最前面的事件,並將事件分發下去以便處理,一般,先發送事件給應用程序的主窗口(keyWindow),主窗口會在視圖層次結構中找到一個最合適的視圖來處理觸摸事件,這也是整個處理過程的第一步。

流程圖(圖1):

事件傳遞

UIWindow 接收到的事件,有的是經過響應者鏈傳遞,找到合適的響應者進行處理;有的不須要傳遞,直接用 first responder 來處理。這裏咱們主要說須要沿着響應者鏈傳遞的過程。

事件的傳遞大體能夠分爲三個階段:

  1. Hit-Test(尋找合適的 view)
  2. Gesture Recognizer(手勢識別)
  3. Response Chain(響應鏈,touch 事件傳遞)

經過手或觸筆觸摸屏幕所產生的事件,都是經過這三步去傳遞的,如前面提到的觸摸事件和按壓事件。

1.Hit-Test(尋找合適的 view)

其實這是肯定第一響應者的過程,第一響應者也就是做爲首先響應這次事件的對象。對於每次事件發生以後,系統會去找能處理這個事件的第一響應者。根據不一樣的事件類型,第一響應者也不一樣:

  • 觸摸事件:被觸摸的那個 view
  • 按壓事件:被聚焦按壓的那個對象
  • 搖晃事件:用戶或者 UIKit 指定的那個對象
  • 遠程事件:用戶或者 UIKit 指定的那個對象
  • 菜單編輯事件:用戶或者 UIKit 指定的那個對象

與加速計、陀螺儀、磁力儀相關的運動事件,是不遵循響應鏈機制傳遞的。Core Motion 會將事件直接傳遞給你所指定的第一響應者。

原理

當點擊一個 view,事件傳遞到 UIWindow 後,會去遍歷 view 層級,直到找到合適的響應者來處理事件,這個過程也叫作 Hit-Test。

既然是遍歷,就會有必定的順序。系統會根據添加 view 的先後順序,肯定 viewsubviews 中的順序,而後根據這個順序將視圖層級轉化爲圖層樹,針對這個樹,使用倒序、深度遍歷的算法,進行遍歷。之因此要倒敘,是由於最頂層的 view 最有可能成爲響應者。

Hit-Test 在代碼中對應的方法爲:

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
// hitTest 內部調用下面這個方法
func point(inside point: CGPoint, with event: UIEvent?) -> Bool
複製代碼

詳細步驟:

  1. keyWindow 接收到 UIApplication 傳遞過來的事件,首先判斷本身可否接受觸摸事件,若是能,那麼判斷觸摸點在不在本身身上
  2. 若是觸摸點在 keyWindow 身上,那麼 keyWindow 會從後往前遍歷本身的子控件(爲了尋找最合適的 view
  3. 遍歷的每個子控件都會重複上面的兩個步驟(1.判斷子控件是否能接受事件;2.觸摸點在不在子控件上)
  4. 如此循環遍歷子控件,直到找到最合適的 view,若是沒有更合適的子控件,那麼本身就是最合適的 view

每當手指接觸屏幕,UIApplication 接收到手指的事件以後,就會去調用 UIWindowhitTest:withEvent:,看看當前點擊的點是否是在 window 內,若是是則繼續依次調用 subViewhitTest:withEvent: 方法,直到找到最後須要的 view。調用結束而且 hit-test view 肯定以後,這個 viewview 上面依附的手勢,都會和一個 UITouch 的對象關聯起來,這個 UITouch 會做爲事件傳遞的參數之一,咱們能夠看到 UITouch 的頭文件中有一個 viewgestureRecognizers 的屬性,就是 hit-test view 和它的手勢。

以下圖(圖2):

Hit-Test 是採用遞歸的方法從 view 層級的根節點開始遍歷,來經過一個例子看一下它是如何工做的(圖3):

UIWindow 有一個 MainViewMainView 裏面有三個 subViewviewAviewBviewC。它們各自有兩個 subView,它們的層級關係是:viewA 在最下面,viewB 在中間,viewC 最上(也就是 addSubview 的順序,越晚 add 進去越在上面),其中 viewAviewB 有一部分重疊。

若是手指在 viewB.1viewA.2 重疊的方面點擊,按照上面的遞歸方式,順序以下圖所示(圖4):

當點擊圖中位置時,會從 viewC 開始遍歷,先判斷點在不在 viewC 上,不在。轉向 viewB,點在 viewB 上。轉向 viewB.2,判斷點在不在 viewB.2 上,不在。轉向 viewB.1,點在 viewB.1 上,且 viewB.1 沒有子視圖了,那麼 viewB.1 就是最合適的 view。遍歷到這裏也就結束了。

實現

來看一下 hitTest:withEvent: 的實現原理,UIWindow 拿到事件以後,會先將事件傳遞給圖層樹中距離最靠近 UIWindow 那一層最後一個 view,而後調用其 hitTest:withEvent: 方法。注意這裏是先將視圖傳遞給 view,再調用其 hitTest:withEvent: 方法,並遵循如下原則:

  • 若是 point 不在這個視圖內,則去遍歷其餘視圖
  • 若是 point 在這個視圖內,可是這個視圖還有子視圖,那麼將事件傳遞給子視圖,而且調用子視圖的 hitTest:withEvent:
  • 若是 point 在這個視圖內,而且這個視圖沒有子視圖,那麼 return self,即它就是那個最合適的視圖
  • 若是 point 在這個視圖內,而且這個視圖沒有子視圖,可是不想做爲處理事件的 view,那麼能夠 return nil,事件由父視圖處理

另外, UIView 有些狀況下是不能接受觸摸事件的:

  • 不容許交互:userInteractionEnabled = NO
  • 隱藏:若是把父控件隱藏,那麼子控件也會隱藏,隱藏的控件不能接受事件
  • 透明度:若是設置一個控件的 alpha < 0.01,會直接影響子控件的透明度。alpha 在 0 到 0.01 之間會被當成透明處理

注:若是父控件不能接受觸摸事件,那麼子控件就不可能接受到事件。

綜上,咱們能夠得出 hitTest:withEvent: 方法的大體實現以下:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    // 是否能響應 touch 事件
    if !isUserInteractionEnabled || isHidden || alpha <= 0.01 { return nil }
    if self.point(inside: point, with: event) {  // 點擊是否在 view 內
        for subView in subviews.reversed() {
            // 轉座標
            let convertdPoint = subView.convert(point, from: self)
            // 遞歸調用,直到有返回值,不然返回 nil
            let hitTestView = subView.hitTest(convertdPoint, with: event)
            if hitTestView != nil {
                return hitTestView!
            }
        }
        return self
    }
    return nil
}
複製代碼

用一張圖來表示 hitTest:withEvent: 的調用過程(圖是 OC 語法)(圖5):

2.Gesture Recognizer(手勢識別)

肯定了最合適的 view,接下來就是識別是何種事件,在觸摸事件中,對應的就是何種手勢。Gesture Recognizer(手勢識別器)是系統封裝的一些類,用來識別一系列常見的手勢,例如點擊、長按等。在上一步中肯定了合適的 view 以後,UIWindow 會將 touches 事件先傳遞給 Gesture Recognizer,再傳遞給視圖。能夠自定義一個手勢驗證一下。

Gesture Recognizer 擁有的狀態以下:

public enum State : Int {
    // 還沒有識別是何種手勢操做(但可能已經觸發了觸摸事件),默認狀態
    case possible   
    // 手勢已經開始,此時已經被識別,可是這個過程當中可能發生變化,手勢操做還沒有完成
    case began
    // 手勢狀態發生改變
    case changed
    // 手勢識別完成(此時已經鬆開手指)
    case ended
    // 手勢被取消,恢復到默認狀態
    case cancelled
    // 手勢識別失敗,恢復到默認狀態
    case failed
    // 手勢識別完成,同 end
    public static var recognized: UIGestureRecognizer.State { get }
}
複製代碼

Gesture Recognizer 有一套本身的 touches 方法和狀態轉換機制。一個手勢老是以 possible 狀態開始,代表它已經準備好開始處理事件。從該狀態開始,開始識別各類手勢,直到它們到達 endedcancelledfailed 狀態。手勢識別器會保持在其中的一個最終狀態,直到當前事件序列結束,此時 UIKit 重置手勢識別器並將其返回 possible 狀態。

再來看看觸摸事件的類型:

  • 長按手勢(UILongPressGestureRecognizer
  • 拖動手勢(UIPanGestureRecognizer
  • 捏合手勢(UIPinchGestureRecognizer
  • 響應屏幕邊緣手勢(UIScreenEdgePanGestureRecognizer
  • 輕掃手勢(UISwipeGestureRecognizer
  • 旋轉手勢(UIRotationGestureRecognizer
  • 點擊手勢(UITapGestureRecognizer

蘋果將手勢識別器分爲兩種大類型,一個是離散型手勢識別器(Discrete Gesture Recognizer),一個是連續型手勢識別器(Continuous Gesture Recognizer)。離散型手勢一旦識別就沒法取消,並且只會調用一次操做事件,而連續型手勢會屢次調用操做事件,而且能夠取消。在以上手勢中,只有點擊手勢(UITapGestureRecognizer)屬於離散型手勢。

離散型手勢識別示意圖(圖6):

連續型手勢識別的狀態轉換通常可分爲三個階段:

  1. 初始事件序列將手勢識別器移動到 beganfailed 狀態
  2. 後續事件將手勢識別器移動到 changedcancelled 狀態
  3. 最終事件將手勢識別器移動到 ended 狀態

以下圖(圖7):

Response Chain(響應鏈、touch 事件傳遞)

識別出手勢以後,就要肯定由誰來響應這個事件了,最有機會處理事件的對象就是經過 Hit-Test 找到的視圖或者第一響應者,若是兩個都不能處理,就須要傳遞給下一位響應者,而後依次傳遞,該過程與 Hit-Test 過程正好相反。Hit-Test 過程是從上向下(從父視圖到子視圖)遍歷,touch 事件處理傳遞是從下向上(從子視圖到父視圖)傳遞。下一位響應者是由響應者鏈決定的,那咱們先來看看什麼是響應者鏈。

Response Chain,響應鏈,通常咱們稱之爲響應者鏈。在咱們的 app 中,全部的視圖都是按照必定的結構組織起來的,即樹狀層次結構,每一個 view 都有本身的 superView,包括 controllertopmost view(即 controllerself.view)。當一個 viewaddsuperView 上的時候,它的 nextResponder 屬性就會被指向它的 superView。當 controller 被初始化的時候,self.view(topmost view) 的 nextResponder 會被指向所在的 controller,而 controllernextResponder 會被指向 self.viewsuperView,這樣,整個 app 就經過 nextResponder 串成了一條鏈,這就是咱們所說的響應者鏈。因此響應者鏈式一條虛擬的鏈,並無一個對象來專門存儲這樣的一條鏈,而是經過 UIResponder 的屬性串聯起來的。

響應者鏈示意圖(圖8):

即(右圖):

  1. 初始視圖(initial view)嘗試處理事件,若是不能處理,則將事件傳遞給其父視圖(superView1
  2. superView1 嘗試處理事件,若是不能處理,傳遞給它所屬的視圖控制器(viewController1
  3. viewController1 嘗試處理事件,若是不能處理,傳遞給 superView1 的父視圖(superView2
  4. superView2 嘗試處理事件,若是不能處理,傳遞給 superView2 所屬的視圖控制器(viewController2
  5. viewController2 嘗試處理事件,若是不能處理,傳遞給 UIWindow
  6. UIWindow 嘗試處理事件,若是不能處理,傳遞給 UIApplication
  7. UIApplication 嘗試處理事件,若是不能處理,拋棄該事件

再附一個蘋果官方的圖(圖9):

UIResponder(響應者)

在 iOS 中,只有繼承於 UIResponder 的對象、或者它自己才能成爲響應者。不少常見的對象均可以相應事件,好比 UIApplicationUIViewController、全部的 UIView(包括 UIWindow)。

UIResponder 提供瞭如下方法來處理事件:

// 觸摸事件
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
@available(iOS 9.1, *)
open func touchesEstimatedPropertiesUpdated(_ touches: Set<UITouch>)

// 運動事件
open func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionCancelled(_ motion: UIEvent.EventSubtype, with event: UIEvent?)

// 遠程控制事件
open func remoteControlReceived(with event: UIEvent?)

// 按壓事件
open func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesChanged(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?)
複製代碼

提供如下屬性和方法來管理響應鏈:

// 負責事件傳遞,默認返回 nil,子類必須實現此方法。
open var next: UIResponder? { get }
// 判斷是否能夠成爲第一響應者
open var canBecomeFirstResponder: Bool { get } // default is NO
// 將對象設置爲第一響應者
open func becomeFirstResponder() -> Bool // default is NO
// 判斷是否能夠放棄第一響應者
open var canResignFirstResponder: Bool { get } // default is YES
// 放棄對象的第一響應者身份
open func resignFirstResponder() -> Bool // default is YES
// 判斷對象是否爲第一響應者
open var isFirstResponder: Bool { get }
複製代碼

補充一下 nextUIResponder 類並不自動保存或設置下一個響應者,該方法的默認實現是返回 nil。子類的實現必須重寫這個方法來設置下一響應者。UIView 的實現是返回管理它的 UIViewController 對象(若是它有)或其父視圖;UIViewController 的實現是返回它的視圖(self.view)的父視圖;UIWindow 的實現是返回 UIApplication

另外說一下 UITouch,對於觸摸事件(對應的對象爲 UITouch),系統提供了四個方法來處理:

open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
@available(iOS 9.1, *)
open func touchesEstimatedPropertiesUpdated(_ touches: Set<UITouch>)
複製代碼

解釋一下 touchesEstimatedPropertiesUpdated(_ touches: Set<UITouch>),當沒法獲取真實的 touches 時,UIKit 會提供一個預估值,並設置到 UITouch 對應的 estimatedProperties 中監測更新。當收到新的屬性更新時,會經過調用此方法來傳遞這些更新值。當使用 Apple Pencil 靠近屏幕邊緣時,傳感器沒法感應到準確的值,此時會獲取一個預估值賦給 estimatedProperties 屬性。不斷去更新數據,直到獲取到準確的值。

上面的前四個方法,是由系統自動調用的:

  • 默認狀況下,當發生一個事件時,view 只接收到一個 UITouch 對象。當你使用多個手指同時觸摸時,會接收多個 UITouch 對象,每一個手指對應一個。多個手指分開觸摸,會調用屢次 touches 系列方法,每一個 touches 裏面有一個 UITouch 對象
  • 若是你想處理一些額外的事件,能夠重寫以上四個方法,處理你想處理的事件。以後不要忘記調用 super.touchesxxx 方法,不然事件處理就中斷於此,不會繼續傳遞

來看一下 UITouch 對象,它保存了事件的相關信息:

// 觸摸事件產生或變化的時間,單位是秒
open var timestamp: TimeInterval { get }
// 當前觸摸事件所處的狀態
open var phase: UITouch.Phase { get }
// 短期內點按屏幕的次數
open var tapCount: Int { get }
// 觸摸產生時所處的視圖
open var view: UIView? { get }
// 觸摸產生時所處的窗口
open var window: UIWindow? { get }
// 依附在 view 上的手勢
open var gestureRecognizers: [UIGestureRecognizer]? { get }
// 使用硬件設備點擊時,以點爲圓心的 touch 半徑,以此肯定 touch 範圍的大小
open var majorRadius: CGFloat { get }
// 半徑公差
open var majorRadiusTolerance: CGFloat { get }

// 一些方法
/** 返回值表示觸摸點在 view 上的位置 調用時傳入的 view 參數爲 nil 的話,返回的是觸摸點在 UIWindow 的位置 */
open func location(in view: UIView?) -> CGPoint
// 記錄了前一個觸摸點的位置
open func previousLocation(in view: UIView?) -> CGPoint
複製代碼

實際運用

以幾個例子來講明事件傳遞與響應在項目中的運用,其實運用主要是圍繞 hitTest:withEvent:pointInside: 的使用,這裏簡單舉個例子。

1.增長視圖的 touch 區域

在實際開發中,有些 button 面積很小,不容易點擊上。這時候你想擴大 button 的響應區域,能夠經過重寫 hitTest:withEvent: 方法實現,以下圖的狀況(圖10):

實現代碼:

class MyButton: UIButton {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if !isUserInteractionEnabled || isHidden || alpha <= 0.01 { return nil }
        let inset : CGFloat = 45 - 78
        let touchRect = bounds.insetBy(dx: inset, dy: inset)
        if (touchRect.contains(point)) {
            for subView in subviews.reversed() {
                let convertdPoint = subView.convert(point, from: self)
                let hitTestView = subView.hitTest(convertdPoint, with: event)
                if hitTestView != nil {
                    return hitTestView!
                }
            }
            return self
        }
        return nil
    }

}
複製代碼

或者直接改 pointIndside 方法:

class MyButton: UIButton {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        return bounds.insetBy(dx: 45-78, dy: 45-78).contains(point)
    }
}
複製代碼

2.搖一搖事件

以前沒作過搖一搖,感受還挺好玩的,就放在這裏,其實很簡單。

import UIKit

class ShakeView : UIView {
    
    override var canBecomeFirstResponder: Bool {  // 記得重寫這個方法
        return true
    }
    
    override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
        if motion == .motionShake {
            print("搖一搖")
        }
    }
    
    override func motionCancelled(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
        if motion == .motionShake {
            print("取消")
        }
    }
    
    override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
        if motion == .motionShake {
            print("結束")
        }
    }
    
}

class ViewController: UIViewController {
    
    lazy var shakeView : ShakeView? = {
        let shakeView = ShakeView(frame: view.bounds)
        shakeView.backgroundColor = #colorLiteral(red: 0.08779912442, green: 0.6471169591, blue: 0.9447124004, alpha: 1)
        return shakeView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 設置支持搖一搖
        UIApplication.shared.applicationSupportsShakeToEdit = true
        
        view.addSubview(shakeView!)
        
        shakeView?.becomeFirstResponder()
        
    }
    
}
複製代碼

總結

來個總結吧。

iOS 中的事件:

  • 觸摸事件
  • 運動事件
  • 遠程控制事件
  • 按壓事件

事件從產生到系統傳遞到 App 的 keyWindow

  1. 手指觸摸屏幕,IOKit.framework 將事件封裝成一個 IOHIDEvent 對象
  2. 將這個對象經過 mach port(IPC 進程間通訊)轉發到 Springboard
  3. Springboard 經過 mach port(IPC 進程間通訊)轉發給當前 App 的主線程
  4. 前臺 App 主線程的 RunLoop 接收到 Springboard 轉發過來的消息以後,觸發對應的 mach portSource1 回調 __IOHIDEventSystemClientQueueCallback()
  5. Source1 回調內部觸發了 Source0 的回調 __UIApplicationHandleEventQueue()
  6. Source0 回調內部,封裝 IOHIDEventUIEvent
  7. Source0 回調內部調用 UIApplication+sendEvent: 方法,將 UIEvent 傳給當前 UIWindow

事件傳遞分爲三步:

  1. Hit-Test(尋找最合適的 view,即第一響應者)
  2. Gesture Recognizer(手勢識別)
  3. Response Chain(響應鏈,傳遞 touch 事件)

1.Hit-Test:

  1. keyWindow 接收到 UIApplication 傳遞過來的事件,首先判斷本身可否接受觸摸事件,若是能,那麼判斷觸摸點在不在本身身上
  2. 若是觸摸點在 keyWindow 身上,那麼 keyWindow 會倒序遍歷本身的子控件
  3. 遍歷的每個子控件都會重複上面兩個操做(1.判斷子控件是否能接受事件;2.觸摸點在不在子控件上)
  4. 如此循環遍歷子控件,直到找到最合適的 view,若是沒有,那麼本身就是最合適的 view

能夠看看圖2。

2.Gesture Recognizer:

UIWindow 會首先將 touches 事件傳遞給 Gesture Recognizer,再傳遞給視圖。

觸摸事件的具體類型有:

  • 長按手勢(UILongPressGestureRecognizer
  • 拖動手勢(UIPanGestureRecognizer
  • 捏合手勢(UIPinchGestureRecognizer
  • 響應屏幕邊緣手勢(UIScreenEdgePanGestureRecognizer
  • 輕掃手勢(UISwipeGestureRecognizer
  • 旋轉手勢(UIRotationGestureRecognizer
  • 點擊手勢(UITapGestureRecognizer

蘋果又將手勢識別器分爲兩大類型,離散型和連續型,上述類型中只有點擊手勢(UITapGestureRecognizer)屬於離散型。

手勢識別器擁有的狀態:

public enum State : Int {
    // 還沒有識別是何種手勢操做(但可能已經觸發了觸摸事件),默認狀態
    case possible   
    // 手勢已經開始,此時已經被識別,可是這個過程當中可能發生變化,手勢操做還沒有完成
    case began
    // 手勢狀態發生改變
    case changed
    // 手勢識別完成(此時已經鬆開手指)
    case ended
    // 手勢被取消,恢復到默認狀態
    case cancelled
    // 手勢識別失敗,恢復到默認狀態
    case failed
    // 手勢識別完成,同 end
    public static var recognized: UIGestureRecognizer.State { get }
}
複製代碼

3.Response Chain

事件沿着響應鏈傳遞,傳遞順序與尋找第一響應者的順序正好相反。

傳遞順序:

  1. 初始視圖(initial view)嘗試處理事件,若是不能處理,則將事件傳遞給其父視圖(superView1
  2. superView1 嘗試處理事件,若是不能處理,傳遞給它所屬的視圖控制器(viewController1
  3. viewController1 嘗試處理事件,若是不能處理,傳遞給 superView1 的父視圖(superView2
  4. superView2 嘗試處理事件,若是不能處理,傳遞給 superView2 所屬的視圖控制器(viewController2
  5. viewController2 嘗試處理事件,若是不能處理,傳遞給 UIWindow
  6. UIWindow 嘗試處理事件,若是不能處理,傳遞給 UIApplication
  7. UIApplication 嘗試處理事件,若是不能處理,拋棄該事件

參考文章

iOS 中的事件響應與處理

深刻淺出iOS事件機制

你真的瞭解UIGestureRecognizer嗎?

官方文檔 About the Gesture Recognizer State Machine

官方文檔 Implementing a Discrete Gesture Recognizer

官方文檔 Implementing a Continuous Gesture Recognizer

官方文檔 Using Responders and the Responder Chain to Handle Events

相關文章
相關標籤/搜索