ソフトウェア設計

SwiftのChain of ResponsibilityパターンとUIResponderチェーン

SwiftでiOSアプリを作っていると、ボタンをタップしたのに、なぜこのビューコントローラがイベントを受け取るのか疑問に思ったことがあるでしょう。

読了 6 分
SwiftのChain of ResponsibilityパターンとUIResponderチェーンのカバー画像

SwiftでiOSアプリを作っていると、ボタンをタップしたのに、なぜこのビューコントローラがイベントを受け取るのか疑問に思ったことがあるでしょう。

タッチイベントが画面のどこを、どのように流れるのかを追うと、その根底にChain of Responsibilityパターンがあることが分かります。

結論から言うと、UIResponderチェーンはGoFデザインパターンの1つであるChain of Responsibilityを、Appleがフレームワークレベルで実装した代表例です。イベントを処理できるオブジェクトが見つかるまで、responderが次のresponderへ責任を渡します。

この記事では、Chain of Responsibilityとは何か、UIResponderチェーンが実際にどう動くのか、そして実務で何が役立つのかを、コードを読み解きながら整理します。

まずは要点

  1. Chain of Responsibilityは、リクエストを処理できるオブジェクトが見つかるまで、複数のオブジェクトが鎖のようにリクエストを渡していくデザインパターンです。
  2. iOSのUIResponderチェーンは、このパターンをフレームワークレベルで実装したものです。
  3. タッチイベントはUIViewからスーパービュー、ビューコントローラ、ウィンドウ、アプリケーションの順に上へ伝わります。
  4. nextプロパティがチェーンの次のリンクを指し、誰も処理しなければイベントはそのまま破棄されます。

Chain of Responsibilityパターンとは?

Chain of Responsibilityは、リクエストの送信側と処理側を疎結合にする振る舞いパターンです。

会社で決裁を回す場面に似ています。担当者が処理できればそこで終わり、できなければ課長へ、課長でも難しければ部長へ回ります。

リクエストを送る側は、最終的に誰が承認するかを知る必要がありません。チェーンの最初のリンクに渡せば、あとは自動的に流れていきます。

このパターンの要点は、各処理オブジェクトが2つだけ知っていればよいことです。自分がリクエストを処理できるか、できない場合は次のオブジェクトが誰か、の2つです。

Swiftで非常に単純化すると、次のような骨格になります。

class Handler {
    var next: Handler?  // チェーンの次のリンク
    func handle(_ request: Int) {
        // 処理できなければ次のオブジェクトへ責任を渡す
        next?.handle(request)
    }
}

1つのnextプロパティでチェーンをつなぎ、処理できなければnextへ渡す構造が、そのままUIResponderに組み込まれています。


UIResponderチェーンはどう動く?

iOSでタッチ、モーション、リモートコントロールなどのイベントを受け取れるオブジェクトは、すべてUIResponderを継承します。UIView、UIViewController、UIWindow、UIApplicationはいずれもUIResponderの子です。

そのため、これらのオブジェクトはそれぞれ1つのプロパティを持っており、その正確な名前がnextです。UIResponderで定義されています。

// UIResponderで定義されたプロパティ
var next: UIResponder? { get }

ユーザーが画面に触れると、システムはまず最も内側にあるタッチされたビューを探します。これはヒットテストと呼ばれ、この時点でfirst responderの候補が決まります。

そのビューがイベントを処理しなければ、nextをたどって上へ進みます。順序はおおむね次のとおりです。

nextをたどって上へ渡される責任のチェーン
nextをたどって上へ渡される責任のチェーン
  • UIView(タッチされたビュー)
  • 上位のスーパービュー
  • そのビューを管理するUIViewController
  • UIWindow
  • UIApplication
  • UIApplicationDelegate

チェーンを最後までたどっても誰もイベントを処理しなければ、イベントは静かに破棄されます。アプリがクラッシュしたりエラーになったりするのではなく、単に無視されます。

ここで面白いのは、ビューのnextが常にスーパービューとは限らないことです。

そのビューがビューコントローラのルートビューなら、nextはスーパービューではなくビューコントローラになります。


これを知ると実務で何が便利?

正直なところ、最初はこの内部構造を知らなくてもアプリは問題なく動きます。しかし理解していると、デバッグの進め方が明らかに変わります。

私が経験した例を紹介します。カスタムビューでタップジェスチャが反応しない問題がありました。

responderがどこで止まるのかを順に追跡したところ、上位ビューのisUserInteractionEnabledがfalseで、そこでチェーンが切れていることをすぐに見つけられました。

カスタムイベントの伝播にも役立ちます。UIResponderチェーンを上に伝わるメッセージを自分で定義すれば、深い階層のビューで発生したアクションを、delegateなしで上位のビューコントローラまで届けられます。

キーボードを閉じるときによく使うendEditing(true)も、実はこのチェーンを利用しています。first responderを見つけてresignFirstResponderを呼び出す仕組みです。

まとめると、UIResponderチェーンを理解すれば、イベントが効かないときにどこで止まったかを論理的に追跡でき、delegateを多用せずにイベントを上へ流す設計も可能になります。

実際にnextプロパティをコードで読み解いた日
実際にnextプロパティをコードで読み解いた日

よくある質問

Q. first responderとresponderチェーンは別物ですか? A. first responderはイベントを最初に受け取る資格を持つ1つのオブジェクトで、responderチェーンはそのfirst responderから上へ続くチェーン全体を指します。

Q. nextは常にスーパービューを指しますか? A. いいえ。通常のビューはスーパービューを指しますが、ビューコントローラのルートビューではnextがビューコントローラになります。

この1回のタップがどのresponderまで上がるのか追ってみました
この1回のタップがどのresponderまで上がるのか追ってみました

Chain of Responsibilityという名前は難しく感じるかもしれませんが、結局は「処理できなければ次へ渡す」という単純な原理です。この記事が、UIResponderチェーンというiOSの隠れた骨格を理解する助けになれば幸いです。次にイベントが効かないバグに遭遇したら、このチェーンを1つずつたどってみてください。

あわせて読みたい