- 原文地址:iOS Responder Chain: UIResponder, UIEvent, UIControl and uses
- 原文做者:Bruno Rocha
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:iWeslie
- 校對者:swants
當我用使用 UITextField 究竟誰是第一響應者? 爲何 UIView 像 UIResponder 同樣進行子類化? 這其中的關鍵又是什麼?html
在 iOS 裏,響應者鏈 是指 UIKit 生成的 UIResponder 對象組成的鏈表,它同時仍是 iOS 裏一切相關事件(例如觸摸和動效)的基礎。前端
響應者鏈是你在 iOS 開發的世界中常常須要打交道的東西,而且儘管你不多須要在除了 UITextField
的鍵盤問題以外直接處理它。瞭解它的工做原理將讓你解決事件相關的問題更加容易,或者說更加富有創造力,你甚至能夠只依賴響應者鏈來進行架構。android
簡而言之,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
發送的,MyViewController
的 myCustomMethod
也會被調用。
當你沒有指定 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 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。