[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 も述語部分にあたると思いますが、 forなどの前置詞があるときはメソッドの引数ラベルの役割を果たします。

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

デリゲートメソッドには大きく3つのケースがあると考えます。

もちろん特殊な例はあると思いますが、経験上ほとんどが下記のケースに当てはまると思います。 そして、それぞれに「述語」の書き方が変わってきます。

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

このケースはシンプルにいうとイベントハンドリングの委譲です。

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

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

記法

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

func textFieldDidEndEditing(_ textField: UITextField)

この例では「テキストフィールド(入力フォーム)の編集が終わったとき」というイベントをハンドルするメソッドになるわけです。

また、willである場合は「編集が終わろうとするとき = フォーカスが外れる前のタイミング」というふうに直感的にわかりやすい英文書になっていることも意識しましょう。

  • did : (何かしらのイベントが)起きたとき
  • will: (何かしらのイベントが)起きたようとしてるとき

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

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

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

そのため、メソッドのシグネチャは、別の引数に判定の材料となる値が渡されることになります。

記法

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

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

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

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

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

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

こちらもメソッドのシグネチャとして、別の引数に判定材料となる値が渡されることになります。

記法

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

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.

こちらの raywenderlich コーディング規約の「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に記載の例だと、何が好ましくて何が好ましくないかの具体的な言及がないので補足します。

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

コーディング規約では、委譲元が UITextField であるならば、最初の引数にはその UITextField が渡されるようにするべきだと言っています。 上記の例でいうと NamePickerView でもそうなっていますね。

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

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

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

func didSelectName(namePicker: NamePickerViewController, name: String)

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

NamePickerなのですから「選ばれた名前だろう」と文脈から察して読めるかもしれませんが、 それはコーディングルールとしては好ましくありません。

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

述語の書き方は前項の「デリゲートメソッドのケースと述語」に従います。

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

didSelectNameの隣にnameが置かれることでnameの意味がブレることがありません。

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

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

func textFieldDidEndEditing(_ textField: UITextField)

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

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

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

numberOfSections(in tableView: UITableView) -> Int

// 以下のようにはしない
tableViewNumberOfSections(_ tableView: UITableView) -> Int

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

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

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

3つ目以降の引数はwithが先頭に省略されていると思って、ラベルを定義していきます。
image-picker-view did select image with size, with scale.

何が嬉しいのか

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

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

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 {
        // 電話番号に関する処理を書く
    }
    :
    などなど
    :
    // ※もちろん、switch文などでもOKと思います。
}

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

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

テキストフィールドとテキストビューが一つの画面に置かれていて、 それぞれの委譲先がそのビューコントローラである場合を例として使います。

ビューコントローラは 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パターンの書き方とその考え方をまとめてみました。

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

それではまた。