軟體設計

Swift 責任鏈模式與 UIResponder 鏈

使用 Swift 開發 iOS App 時,相信大家都曾疑惑過:明明按下的是按鈕,為什麼會是這個視圖控制器收到事件?

閱讀 5 分鐘
Swift 責任鏈模式與 UIResponder 鏈 封面圖

使用 Swift 開發 iOS App 時,相信大家都曾疑惑過:明明按下的是按鈕,為什麼會是這個視圖控制器收到事件?

深入追蹤觸控事件在畫面中如何流動,就會發現底層其實採用了責任鏈模式。

先說結論,UIResponder 鏈是 Apple 在框架層級實作責任鏈模式(Chain of Responsibility)的代表案例,而責任鏈模式是 GoF 設計模式之一。responder 會逐一將責任傳給下一個 responder,直到找到能處理事件的物件。

本文會拆解程式碼,整理責任鏈模式的概念、UIResponder 鏈的實際運作方式,以及理解這些內容對實務開發的幫助。

先看重點摘要

  1. 責任鏈模式是一種設計模式,會讓多個物件像鏈條一樣傳遞請求,直到找到能處理請求的物件。
  2. iOS 的 UIResponder 鏈,就是在框架層級實作這個模式。
  3. 觸控事件會依序從 UIView 傳到上層視圖、視圖控制器、視窗,最後到應用程式。
  4. next 屬性指向鏈中的下一個環節;如果沒有人處理事件,事件就會被直接丟棄。

什麼是責任鏈模式?

責任鏈模式是一種行為型模式,能讓請求傳送端與處理端保持鬆散耦合。

簡單來說,就像公司裡送簽核一樣。助理經理能處理就到此為止,不能處理就交給經理,經理也無法處理時再往上交給協理。

提出請求的人不必知道最後由誰核准,只要把請求交給鏈上的第一個環節,它就會自行往下傳遞。

這個模式的核心是,每個處理物件只需要知道兩件事:自己能不能處理這個請求,以及不能處理時下一個物件是誰。

用 Swift 極度簡化後,大致會是這樣的骨架。

class Handler {
    var next: Handler?  // 鏈中的下一個環節
    func handle(_ request: Int) {
        // 無法處理時將責任傳給下一個物件
        next?.handle(request)
    }
}

只靠一個 next 屬性串起鏈條,無法處理時就交給 next,這個結構原封不動地整合在 UIResponder 中。


UIResponder 鏈如何運作?

在 iOS 中,所有能接收觸控、動作、遠端控制等事件的物件,都會繼承 UIResponder。UIView、UIViewController、UIWindow 和 UIApplication 全都是 UIResponder 的子類別。

因此,這些物件各自都有一個屬性,而它的正確名稱就是 next。這個屬性定義在 UIResponder 中。

// UIResponder中定義的屬性
var next: UIResponder? { get }

使用者觸碰畫面時,系統會先找出最內層、實際被觸碰的視圖。這稱為命中測試,也會在此時決定 first responder 候選物件。

如果該視圖沒有處理事件,系統就會沿著 next 向上傳遞,大致順序如下。

沿著 next 一路向上傳遞的責任鏈
沿著 next 一路向上傳遞的責任鏈
  • UIView(被觸碰的視圖)
  • 上層的各個超級視圖
  • 管理該視圖的 UIViewController
  • UIWindow
  • UIApplication
  • UIApplicationDelegate

即使一路走完整條鏈都沒有人處理事件,事件也只會無聲地被丟棄。App 不會當掉或產生錯誤,只是單純忽略它。

有趣的是,視圖的 next 不一定永遠是超級視圖。

如果該視圖是視圖控制器的根視圖,next 就會是視圖控制器,而不是超級視圖。


了解這點對實務有什麼好處?

老實說,一開始即使不了解這個內部結構,App 也能正常運作。但理解之後,除錯方式確實會不一樣。

分享一個我遇過的案例:自訂視圖中的點按手勢沒有反應。

依序追蹤事件在哪個 responder 停下來後,很快就找到原因:上層視圖的 isUserInteractionEnabled 是 false,責任鏈因此在那裡中斷。

自訂事件傳遞也很實用。你可以自行定義沿著 UIResponder 鏈向上傳遞的訊息,讓深層視圖產生的動作不必透過 delegate,就能傳到上層視圖控制器。

收起鍵盤時常用的 endEditing(true),其實也運用了這條鏈。它會找到 first responder,並呼叫 resignFirstResponder。

總結來說,理解 UIResponder 鏈後,遇到事件沒有反應時,就能有邏輯地追蹤中斷位置,也能避免濫用 delegate,設計出將事件向上傳遞的架構。

實際在程式碼中拆解 next 屬性的那一天
實際在程式碼中拆解 next 屬性的那一天

常見問題

Q. first responder 和 responder 鏈是不同的東西嗎? A. first responder 是唯一有資格優先接收事件的物件;responder 鏈則是從 first responder 向上延伸的整條鏈。

Q. next 一定指向超級視圖嗎? A. 不一定。一般視圖會指向超級視圖,但視圖控制器的根視圖,其 next 會是視圖控制器。

我追蹤了這一次點按究竟會沿著 responder 上傳到哪裡
我追蹤了這一次點按究竟會沿著 responder 上傳到哪裡

責任鏈模式這個名稱看起來可能很難,但核心其實只有一個簡單原則:無法處理時就交給下一個物件。希望本文能幫助你理解 UIResponder 鏈這個 iOS 隱藏的骨架。下次遇到事件沒有反應的 Bug,請試著逐個環節追蹤這條鏈。

延伸閱讀