[Swift] Delegate Protocol でメソッドを定義するときの書き方と考え方について

iOSアプリ開発初学者には、なんとなくとっつきにくい `Delegate`パターンの書き方とその考え方をまとめました。
2022.03.16

はじめに

CX事業本部モバイル事業部の中安です。 久々にブログを書きます。

iOSアプリ開発のソースコードでは、デザインパターンの Delegate パターンがよく使われています。

主に標準UI系クラス。たとえばテーブルビューや、コレクションビュー、テキストビューなどでよく目にすると思います。 もちろんUI系に限らず、その他にもXMLパーサなどでも使用されていますし、 サードパーティ製ライブラリのクラスでも標準APIに従う形で Delegate パターンで実装されているものも多いです。

今回は、Delegate パターンに従って Protocol を自作するときの定義方法についてのお話になります。 iOSアプリ開発を始めたばかりの方のお役に立てれば幸いです。

Delegateパターンについて

そもそも「Delegate パターンとは」という話なのですが、 詳細はそのような記事が世の中にはたくさんありそうなので、そちらを参照いただけるとよいと思います。

簡単にいうと、あるオブジェクトが本来行うべき業務(ロジック)の一部を別のオブジェクトに委ねる設計パターンです。

こういう設計にしておくことで、開発者がそのクラスのオブジェクトを使う時に多様な使い方や制御ができ、 汎用性が増すことになります。

デリゲートメソッドの基本書式

ここでは、Delegate パターンに従って作成された Protocol に定義されるメソッドインターフェイスのことを「デリゲートメソッド」と呼ばせていただきます。 このデリゲートメソッドの詳しい書き方は後述しますが、 基本的には「主語 + 述語」という形式で表記されます。

たとえば

// ※横に長いので引数ごとに改行しています
func webView(
  _ webView: WKWebView, 
  didReceiveServerRedirectForProvisionalNavigation:WKNavigation!
)

こちらは、iOSの標準Webビュー WKWebView のデリゲートメソッドのひとつですが、

  • 主語: web view 「ウェブビューが」
  • 述語: did receive server redirect 「サーバーのリダイレクトを受信したとき」

で構成されてます。

for provisional navigation も述語部分にあたるかもしれませんが、 これはどちらかというとメソッドの引数ラベルに近い役割を果たしていると思われます。

デリゲートメソッドのケースと述語

デリゲートメソッドには大きくは3つのケースがあると考えます。 特殊な例はもちろんあると思いますが、経験上ほとんどが下記のようなケースに当てはまると思います。 そして、それぞれに「述語」の書き方が変わってきます。

1.あるイベントの処理を委譲先に任せたいとき

このケースはシンプルにいうとイベントハンドリングです。 あらかじめ委譲元で発生するイベントの種類をデリゲートメソッドに定義しておいて、 委譲先にイベント発火時の処理を委ねるというわけです。

これで委譲元からイベント発火時に起きる事象への関心が外れ、 アプリ特有のロジックとは疎結合な状態になるはずです。

記法

このケースでは、did / will + 動詞 + 目的語 で定義されることが多いです。

func textFieldDidEndEditing(_ textField: UITextField)

この場合は「テキストフィールド(入力フォーム)の編集が終わったとき」というイベントをハンドルするメソッドになるわけです。 これがwillである場合は「編集が終わろうとするとき = フォーカスが外れる前のタイミング」というふうに直感的にわかりやすい英文書になっていることも意識しましょう。

2.ある判定を委譲先に任せたいとき

このケースは、委譲元の処理内で判定するべきことを委譲先に判断させる場合に使用されます。

そのオブジェクト全体にかかるフラグ値であれば、パブリックなプロパティを用意しておけばいいわけですが、 ある条件のもとでフラグ値が変わるものに関してはデリゲートメソッドを使用します。

そのためメソッドのシグネチャとしては、別の引数(第2引数など)に判定の基準となる値が渡されることがほとんどだと思います。

記法

このケースでは、should + 動詞 + 目的語 で、委譲元が何をすべきかを記述することが多いです。 (is〜 はあまり使われません)

func collectionView(
  _ collectionView: UICollectionView, 
  shouldHighlightItemAt indexPath: IndexPath) -> Bool

この場合は「コレクションビューが(このインデックパスのときに)セルにハイライトをするべきか」という 判断を仰ぐメソッドになります。

3.あるオブジェクトまたはパラメータを代わりに渡してほしいとき

このケースでは、委譲元で必要な情報やオブジェクトの提供を委譲先に任せたい場合に使用されます。

たとえばテーブルビューの行数やセル表示はこれに当たります。 「どんな見た目のセル」を「いくつ出すか」は委譲先に委ねられているというわけです。

こちらもメソッドのシグネチャとして、別の引数(第2引数など)に提供する情報の元となる値が渡されることがほとんどだと思います。

記法

このケースでは、名詞 を使って、委譲元が何が欲しいのかを記述することが多いです。

UITableViewDataSourceにおいて実装が必須であるデリゲートメソッドの例を見てみましょう。

func tableView(
  _ tableView: UITableView,
   numberOfRowsInSection section: Int) -> Int

func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath) -> UITableViewCell

上の例は「テーブルビューが(このセクションインデックスのときに)セルを何行表示するか」というメソッドになります。

下の例では「テーブルビューが(このインデックパスのときに)どのようなセルを表示するか」というメソッドになります。

述語に当たる部分が名詞で始まっているのが分かると思います。 そして、戻り値としてその名詞に当たるものを返すように指示されています。

デリゲートメソッドのコーディングルール

デリゲートパターンを採用するときに、 標準APIを参考にして、何となく真似をしながら自作デリゲートプロトコルを定義している方もいるかも知れません。

ここまでデリゲートメソッドをいくつか見てきて、コーディングそのもののルールも何となくお分かりかと思いますが、 あらためてコーディング規約ではどのように言われているのか確認したいと思います。

The Official raywenderlich.com Swift Style Guide.

この内容の「Naming - Delegate」の箇所を参照すると

Delegates
When creating custom delegate methods, an unnamed first parameter should be the delegate source. (UIKit contains numerous examples of this.)

デリゲート
カスタムなデリゲートメソッドを作成するときは、最初の無名引数は委譲元にするべきです。(UIKitでは多数の例があります)

// 好ましい
func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)
func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool

// 好ましくない
func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool

raywenderlichのREADMEだと、何が好ましくて何が好ましくないかの具体的な言及がないので補足します。

デリゲートのメソッドの最初の引数は委譲元にする

前述では委譲元が UITextField であるならば、最初の引数にはその UITextField が渡されるように書くべきだと言っています。 上記の例でいうと NamePickerView でもそうなっていますね。

また、「最初の無名引数」と書いていますので、_を付けてラベルを省略可能にしておかなければいけません。 (「好ましくない」の1つめは、_が付いていないので規約違反というわけです)

誰が何を委譲するのかを分かるようにする

「好ましくない」の1つめの第2引数は nameと定義されています。

func didSelectName(namePicker: NamePickerViewController, name: String)

このメソッドを見ただけでは「nameは何のnameなんだ?」ということになりませんか。

NamePickerなんだから「選ばれた名前」だろとコンテキストから読み取れるかもしれませんが、 それはコーディングルールとしては好ましくないと思います。

この委譲元クラスとデリゲートを使う開発者のためにも、 基本書式である「主語 + 述語」に従ってシグネチャを定義していきましょう。 述語の書き方は前項の「デリゲートメソッドのケースと述語」に従います。

func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)

委譲先に自分自身の参照以外に渡すものがないとき

委譲元が委譲先に自分自身の参照以外に渡すものがないときはメソッド名自体に述語を書きます。 ただし、「最初の引数は委譲元にする」という原則があるので、引数は必ず取ることになります。

func textFieldDidEndEditing(_ textField: UITextField)

この例では 「textFielddidEndEditing したとき」 というイベントタイミングを指し示しますが、 textField以外に委譲先に渡すものがないので、メソッド名にDidEndEditingが主語に次いで書かれています。

しかしながら、名詞が先頭に来る「あるオブジェクトまたはパラメータを代わりに渡してほしいとき」のケースのときは、 メソッド名の先頭に名詞が置かれ、inforなどの前置詞ラベルで委譲元の参照を渡すという例外規約が存在します。

下記のテーブルビューのセクション数を返させるメソッドがそれにあたります。

numberOfSections(in tableView: UITableView) -> Int

委譲先にいくつもの引数が渡されるとき

委譲先に渡す情報(引数)が増える場合は、通常のメソッド定義と同じように書いていきます。

func imagePickerView(
    _ imagePickerView: ImagePickerView, 
    didSelectImage image: UIImage, 
    size: CGSize, 
    scale: CGFloat
)

3つ目以降の引数はwithが先頭に省略されていると思って、ラベルを定義していきます。

何が嬉しいのか

ここまで記法について書いてきましたが、デリゲートメソッドはこのように記述することでどのようなメリットがあるのかをまとめたいと思います。

委譲元によって処理を分岐できる

1つのビューコントローラに、いくつかの入力フォーム(UITextField)があるとします。 このようなUIは決して珍しいものではないでしょう。

たとえば「氏名」「電話番号」「住所」を入れるフォームが並んでいる場合です。

class ViewController: UIViewController {
    @IBOutlet private var nameTextField: UITextField!
    @IBOutlet private var phoneTextField: UITextField!
    @IBOutlet private var addressTextField: UITextField!
}

extension ViewController: UITextViewDelegate {
    
    func textViewDidEndEditing(_ textView: UITextView) {
        // ※(1)
    }
}

画面上のフォームの編集が終わった時にイベントがハンドリングされ、※(1)の場所に入ってきます。 しかし、渡される引数はtextViewであり、定義された3つのどのフォームなのかが判断が付きません。

そこで、textViewIBOutletで定義したオブジェクトを比較することで、分岐ができるというわけです。

func textViewDidEndEditing(_ textView: UITextView) {
    if textView == nameTextField {
        // 氏名に関する処理を書く
    }
    if textView == phoneTextField {
        // 電話番号に関する処理を書く
    }
    :
    などなど
    :
}

まとめると、同一クラスの複数のオブジェクトの委譲先に単一のオブジェクトがなる可能性があるので、分岐のために委譲元の参照を渡すのが原則になっているわけです。

重複しにくいメソッドをクラス内に定義できる

テキストフィールドとテキストビューが1画面に置かれていて、 それぞれのデリゲートがそのビューコントローラである場合を考えてみます。

ビューコントローラは UITextFieldDelegateUITextViewDelegateの2つのデリゲートプロトコルを実装していくことになります。

それぞれのプロトコルはこのようなシグネチャで定義されています (少し簡略化してます)

protocol UITextFieldDelegate {
    func textFieldDidEndEditing(_ textField: UITextField)
}

protocol UITextViewDelegate {
    func textViewDidEndEditing(_ textView: UITextView)
}

これがもし、デリゲートプロトコルのコーディング規約を破って下記のように定義されていたらどうでしょうか。

protocol UITextFieldDelegate {
    func didEndEditing()
}

protocol UITextViewDelegate {
    func didEndEditing()
}

この2つのプロトコルをビューコントローラが実装した時点で、名前が衝突してしまいます。エラーが発生してビルドができなくなることでしょう。 つまり、名前空間の代わりのような役割もメソッドのシグネチャで果たしていると言えます。

自作でデリゲートプロトコルを作る際も、このあたりを意識して作っておいたほうが良いと思います。

まとめ

  • iOSアプリ開発の標準的な設計パターン、Delegateパターンを使いこなそう
  • 委譲元の参照を渡すことは、委譲先にメリットをもたらすので原則に従って必ず渡そう
  • デリゲートにおける3つの作用を「述語」とともに覚えよう
    • イベントハンドリング: did/will + 動詞
    • 委譲先に判断を委ねる: should + 動詞
    • 委譲先から情報をもらう: 名詞
  • 使用時を想定してコーディング規約に従おう
    • 同一クラスの複数のオブジェクトの委譲先に単一のオブジェクトがなる可能性がある
    • 似た動作の別クラスと名前衝突する可能性がある

というわけで、iOSアプリ開発を始めた頃は、 なんとなくとっつきにくい Delegateパターンの書き方とその考え方をまとめてみました。

どなたかの役に立ちますように。

それではまた。