[譯] iOS 響應者鏈 UIResponder、UIEvent 和 UIControl 的使用

當我用使用 UITextField 究竟誰是第一響應者? 爲何 UIView 像 UIResponder 同樣進行子類化? 這其中的關鍵又是什麼?html

在 iOS 裏,響應者鏈 是指 UIKit 生成的 UIResponder 對象組成的鏈表,它同時仍是 iOS 裏一切相關事件(例如觸摸和動效)的基礎。前端

響應者鏈是你在 iOS 開發的世界中常常須要打交道的東西,而且儘管你不多須要在除了 UITextField 的鍵盤問題以外直接處理它。瞭解它的工做原理將讓你解決事件相關的問題更加容易,或者說更加富有創造力,你甚至能夠只依賴響應者鏈來進行架構。android

UIResponder、UIEvent 和 UIControl

簡而言之,UIResponder 實例對象能夠對隨機事件進行響應並處理。iOS 中的許多東西諸如 UIView、UIViewController、UIWindow、UIApplication 和 UIApplicationDelegate。ios

相反,UIEvent 表明一個單一併只含有一種類型和一個可選子類的 UIKit 事件,這個類型能夠是觸摸、動效、遠程控制或者按壓,對應的子類具體一點多是設備的搖動。當檢測到一個系統事件,例如屏幕上的點擊,UIKit 內部建立一個 UIEvent 實例而且經過調用 UIApplication.shared.sendEvent() 把它派發到系統事件隊列。當事件被從隊列中取出時,UIKit 內部選出第一個能夠處理事件的 UIResponder 並把它發送到對應的響應者。這個選擇過程當事件類型不一樣的時候也會有所變化,其中觸摸事件直接發送到被觸摸的 View,其餘種類的事件將會被派發給一個所謂的 第一響應者git

爲了處理系統事件,UIResponder 的子類能夠經過重寫一些對應的方法從而讓它們可處理具體的 UIEvent 類型:github

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?)
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?)
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?)
複製代碼

在某種程度上,你能夠將 UIEvents 視爲通知。雖然 UIEvents 能夠被子類化而且 sendEvent 能夠被手動調用,但它們並不真正意味着能夠這麼作,至少不是經過正常方式。因爲你沒法建立自定義類型,派發自定義事件會出現問題,由於非預期的響應者可能會錯誤地 「處理」 你的事件。儘管如此,你仍然可使用它們,除了系統事件,UIResponder 還能夠以 Selector 的形式響應任意 「事件」。swift

這種方法的誕生給 macOS 應用程序提供了一種簡單的方法來響應 「菜單」 的操做,例如選擇、複製還有粘貼,由於 macOS 中存在多個窗口使得簡單的代理難以實現。在任何狀況下,它們也可用於 iOS 以及自定義操做,這正是相似 UIButton 之類的 UIControl 能夠在觸摸後派發事件。看一下以下的一個按鈕:後端

let button = UIButton(type: .system)
button.addTarget(myView, action: #selector(myMethod), for: .touchUpInside)
複製代碼

雖然 UIResponder 能夠徹底檢測觸摸事件,但處理它們並不是易事。 那你要如何區分不一樣類型的觸摸事件呢?架構

這就是 UIControl 擅長的地方,這些 UIView 的子類把處理觸摸事件的過程進行抽象,並揭示了爲特定的觸摸分配事件的能力。app

在內部,觸摸此按鈕會產生如下結果:

let event = UIEvent(...) //包含觸摸位置和屬性的UIKit生成的觸摸事件。
//派發一個觸摸事件。
//經過 `hitTest()` 肯定哪一個 UIView 被 選中。
//由於選擇了 UIControl,因此直接調用:
UIApplication.shared.sendAction(#selector(myMethod), to: myView, from: button, for: event)
複製代碼

當一個特定的目標被髮送到 sendAction 時,UIKit 將直接嘗試在所需的目標上調用所需的 Selector,若是它沒有實現直接就崩潰,可是若是目標爲 nil 又怎麼辦呢?

final class MyViewController: UIViewController {
    @objc func myCustomMethod() {
        print("SwiftRocks!")
    }

    func viewDidLoad() {
        UIApplication.shared.sendAction(#selector(myCustomMethod), to: nil, from: view, for: nil)
    }
}
複製代碼

若是你運行它,你會看到即便事件是從沒有 target 的普通 UIView 發送的,MyViewControllermyCustomMethod 也會被調用。

當你沒有指定 target 時,UIKit 將搜索可以處理此操做的 UIResponder,就像以前在處理簡單的 UIEvent 示例中同樣。在這種狀況下,可以處理動做與如下 UIResponder 方法有關:

open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool
複製代碼

默認狀況下,此方法只檢查響應者是否實現了實際的方法。 「實現」 方法能夠經過三種方式完成,具體取決於你須要多少信息(這適用於 iOS 中的任何原生 target/action 的控件):

func myCustomMethod()
func myCustomMethod(sender: Any?)
func myCustomMethod(sender: Any?, event: UIEvent?)
複製代碼

如今,若是響應者沒有實現該方法怎麼辦?在這種狀況下,UIKit 就會使用如下 UIResponder 方法來肯定如何繼續:

open func target(forAction action: Selector, withSender sender: Any?) -> Any?
複製代碼

默認狀況下,這將返回 另外一個可能能夠 處理所需的操做的 UIResponder。此步驟將重複執行,直處處理完事件或沒有其餘選擇爲止。可是響應者如何知道把操做的路由導向誰呢?

響應者鏈

如開頭所述,UIKit 經過動態管理 UIResponder 對象的鏈表來處理這個問題。所謂的 第一響應者 只是鏈表的頭節點,若是響應者沒法處理特定的事件,則事件被遞歸地發送給鏈表的下一個響應者,直到某個響應者能夠處理該事件或者鏈表遍歷結束。

雖然查看實際的第一響應者是受 UIWindow 中的私有 firstResponder 屬性的保護,但你能夠經過檢查 next 屬性是否有值來檢查任何給定響應者的響應者鏈:

extension UIResponder {
    func responderChain() -> String {
        guard let next = next else {
            return String(describing: self)
        }
        return String(describing: self) + " -> " + next.responderChain()
    }
}

myViewController.view.responderChain()
// MyView -> MyViewController -> UIWindow -> UIApplication -> AppDelegate
複製代碼

在上一個 UIViewController 處理 action 的例子中,UIKit 首先將事件發送給 UIView 第一響應者,但因爲它沒有實現 myCustomMethod,view 將事件發給下一個響應者,正好下一個 UIViewController 實現了所需方法。

雖然在大多數狀況下,響應者鏈符合子視圖的結構順序,但你能夠對其進行自定義以更改常規流程順序。除了可以重寫 next 屬性以返回其餘內容以外,你還能夠經過調用 becomeFirstResponder() 強制 UIResponder 成爲第一響應者,並經過調用 resignFirstResponder() 來取消。這一般與 UITextField 結合使用以顯示鍵盤,UIResponders 能夠定義一個可選的 inputView 屬性,使得鍵盤僅在它是第一響應者時顯示。

響應者鏈自定義用途

雖然響應者鏈徹底由 UIKit 處理,但你可使用它來幫助解決通訊或代理中的問題。

在某種程度上,您能夠將 UIResponder 的操做視爲一次性通知。想一想任何一個應用程序,幾乎每一個 view 均可以添加閃爍效果。來導航用戶在教程中如何操做。當觸發此操做時,如何確保只有當前活動的視圖閃爍呢?可能的解決方案以下之一是使每一個 view 遵循一個協議,或者使用除了 "currentActiveView" 以外每一個 view 都須要忽略的通知,但響應者操做容許你不經過代理並用最少的編碼來實現這一點:

final class BlinkableView: UIView {
    override var canBecomeFirstResponder: Bool {
        return true
    }

    func select() {
        becomeFirstResponder()
    }

    @objc func performBlinkAction() {
        //閃爍動畫
    }
}

UIApplication.shared.sendAction(#selector(BlinkableView.performBlinkAction), to: nil, from: nil, for: nil)
//將精確地讓最後一個調用了 select() 的 BlinkableView 進行閃爍。
複製代碼

這與常規通知很是類似,不一樣之處在於通知會觸發註冊它們的每一個對象,而這個方法只會觸發在響應鏈上最早被查找到的 BlinkableView 對象。

如前所述,甚至能夠用此方法進行架構。這是 Coordinator 結構的框架,它定義了一個自定義類型的事件並將自身注入到響應者鏈中:

final class PushScreenEvent: UIEvent {

    let viewController: CoordenableViewController

    override var type: UIEvent.EventType {
        return .touches
    }

    init(viewController: CoordenableViewController) {
        self.viewController = viewController
    }
}

final class Coordinator: UIResponder {

    weak var viewController: CoordenableViewController?

    override var next: UIResponder? {
        return viewController?.originalNextResponder
    }

    @objc func pushNewScreen(sender: Any?, event: PushScreenEvent) {
        let new = event.viewController
        viewController?.navigationController?.pushViewController(new, animated: true)
    }
}

class CoordenableViewController: UIViewController {

    override var canBecomeFirstResponder: Bool {
        return true
    }

    private(set) var coordinator: Coordinator?
    private(set) var originalNextResponder: UIResponder?

    override var next: UIResponder? {
        return coordinator ?? super.next
    }

    override func viewDidAppear(_ animated: Bool) {
        //在 viewDidAppear 填寫信息以確保 UIKit
        //已配置此 view 的下一個響應者。
        super.viewDidAppear(animated)
        guard coordinator == nil else {
            return
        }
        originalNextResponder = next
        coordinator = Coordinator()
        coordinator?.viewController = self
    }
}

final class MyViewController: CoordenableViewController {
    //...
}

//在 app 的起其餘任何位置:

let newVC = NewViewController()
UIApplication.shared.push(vc: newVC)
複製代碼

這讓 CoordenableViewController 都持有對其原始下一個響應者(window)的引用,可是它重寫了 next 讓它指向 Coordinator,然後者又將 window 指向下一個響應者。

// MyView -> MyViewController -> **Coordinator** -> UIWindow -> UIApplication -> AppDelegate
複製代碼

這容許 Coordinator 接收系統事件,並經過定義一個新的包含了有關新 view controller 信息的 PushScreenEvent,咱們能夠調用由這些 Coordinators 處理的 pushNewScreen 事件來刷新屏幕。

有了這個結構,UIApplication.shared.push(vc: newVC) 能夠在 app 中的 任何地方 調用,而不須要單個代理或單例,由於 UIKit 將確保只通知當前的 Coordinator 這個事件,這得多虧了響應者鏈。

這裏顯示的例子很是理論化,但我但願這有助於你理解響應者鏈的目的和用途。

你能夠在 Twitter 上關注本文做者 — @rockthebruno,有更多建議也能夠分享。

官方參考文檔

使用響應者和響應者鏈來處理事件

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索