LoginSignup
4
7

More than 1 year has passed since last update.

【Swift】protocolとDelegateパターンの基本について

Last updated at Posted at 2023-02-14

はじめに

どうも、プログラミング未経験で学習中のはるさんです。
今回はDelegateパターンについて説明していきたいと思います。

デリゲートパターンは様々なアプリ開発において用いられる記述方法です。

しかし、他の基礎構文の定義方法のように単純ではなく、初学者にとって
難しい内容です。かく言う筆者もプログラミングスクールの課題で
ビジネスロジックに取り組む際にこのデリゲートパターンを用いたのですが、
かなり苦戦しました。
そんな難解なデリゲートパターンを克服すべく執筆しております。

今回はデリゲートパターンの解説をするためにそもそものプロトコルについて
簡単に解説し、本題に入っていきたいと思います。

1. Protocolの基本

1-1 プロトコルとは

プロトコルとは複数の型(クラスなど)で、共通のメソッド(関数)や
プロパティ(変数や定数など)のインターフェースを定義することができる構文です。

インターフェースとはパソコンで言うところの「USB端子」「電源ポート」「HDMI端子」
といった
入力する部分のことです。
ざっくり言うと、入力項目は決めれるけど内容は決められないと言うことです。
関数であれば関数名は決めるけど、処理内容までは定義しないと言うことです。

1-2 定義方法

プロトコルはprotocolキーワードに続いてプロトコル名を記述して宣言し、
{}(中括弧)内にプロパティやメソッドを定義します。
今回は例題として「ネコ型ロボット」「イヌ型ロボット」というクラスを定義し
それに
「ロボット」プロトコルを適用していきたいと思います。


// プロトコル「ロボット」
protocol Robot {}

// クラス「ネコ型ロボット」
class CatRobot {}

// クラス「イヌ型ロボット」
class DogRobot {}

1-3 準拠方法

プロトコルをクラスに適用することを準拠と言います。
準拠させるにはクラス名に続いて(コロン)を記述し、続けてプロトコル名を記述します。


// プロトコル「ロボット」
protocol Robot {}

// クラス「ネコ型ロボット」 → プロトコル「ロボット」に準拠
class CatRobot: Robot {}

// クラス「イヌ型ロボット」 → プロトコル「ロボット」に準拠
class DogRobot : Robot{}

1-4 メソッドを定義

メソッドを定義します。最初に触れたプロトコルではメソッド名だけを定義し、
それぞれのクラスで処理内容を定義していきます。


// プロトコル「ロボット」
protocol Robot {
    // メソッド喋る
    func say()
}

// クラス「ネコ型ロボット」 → プロトコル「ロボット」に準拠
class CatRobot: Robot {
    func say() {
        print("にゃーにゃー!")
    }
}

// クラス「イヌ型ロボット」 → プロトコル「ロボット」に準拠
class DogRobot : Robot{
    func say() {
        print("わんわん")
    }
}

1-5 プロトコルによるメソッドの特徴

プロトコルに定義したメソッドはクラス毎に処理内容を設定することができます。
例えばイヌ型ロボットは喋るsay()処理をした後に走李さる、ネコ型ロボットは
座って寝るなど別々の処理が設定できます。

// プロトコル「ロボット」
protocol Robot {
    // メソッド喋る
    func say()
}

// クラス「ネコ型ロボット」 → プロトコル「ロボット」に準拠
class CatRobot: Robot {
    func say() {
        print("わんわん!")
        // メソッド走り去る
        runAway()
    }
    func runAway() {}
}

// クラス「イヌ型ロボット」 → プロトコル「ロボット」に準拠
class DogRobot : Robot {
    func say() {
        print("にゃーにゃー")
        // メソッド座って寝る
        sitAndSleep()
    }
    func sitAndSleep() {}
}

1-6 準拠の約束事

プロトコルに準拠する場合には準拠したクラスは必ずプロトコルに定義されている
メソッドやプロパティを定義しなければいけません。
試しにプロトコル「ロボット」に新たなプロパティnameを追加します。
プロパティを
プロトコルに定義する際にはvar 変数名 : { get }という
記述をします。

// プロトコル「ロボット」
protocol Robot {
    // メソッド喋る
    func say()
    var name: String { get }
}

// クラス「ネコ型ロボット」 → プロトコル「ロボット」に準拠
class CatRobot: Robot {
    func say() {
        print("にゃーにゃー!")
    }
}

// クラス「イヌ型ロボット」 → プロトコル「ロボット」に準拠
class DogRobot : Robot{
    func say() {
        print("わんわん")
    }
}

スクリーンショット 2023-02-14 1.46.33.png
すると上記のようなエラーが発生します。

  • エラー原文→ Type 'CatRobot' does not conform to protocol 'Robot'
  • エラー翻訳→ タイプ「DogRobot」はプロトコル「Robot」に準拠していません

これを回避する為に両方に同じプロパティを定義します。この時、プロパティに入る
値はそれぞれのクラスで設定できます。

// プロトコル「ロボット」
protocol Robot {
    // メソッド喋る
    func say()
    // プロパティ 型の名前
    var typeName: String { get }
}

// クラス「ネコ型ロボット」 → プロトコル「ロボット」に準拠
class CatRobot: Robot {
    func say() {
        print("にゃーにゃー!")
    }
    // プロパティ 型の名前
    var typeName: String = "ネコ型ロボット"
}

// クラス「イヌ型ロボット」 → プロトコル「ロボット」に準拠
class DogRobot : Robot {
    func say() {
        print("わんわん")
    }
    // プロパティ 型の名前
    var typeName: String = "イヌ型ロボット"
}

2. Delegateパターンの基本

2-1 デリゲートパターンの概要

デリゲートパターンはswiftにおける通知パターンの一つです。
Delegateとは日本語で委譲や委任と言います。デリゲートパターンとはあるオブジェクト(クラス)
の処理を別のオブジェクトに代替えさせます。そしてその処理が終わったら、オブジェクト元に
通知を返すという形です。
処理の内容は先に述べたプロトコルに定義したメソッドに記述します。
スクリーンショット 2023-02-14 10.00.30.png

2-2 実装方法

ではここから実際の例に沿って実装していきます。今回は洗濯機を例題にしたいと思います。
*「洗濯機に洗濯物を入れて、規定時間の洗濯をすると終了音でお知らせしてくれる」*
という機能を実装していきます。

徐々にコード量が増えていきます。コードは基本、クラス単位で載せるようにしています。
中盤から追記した箇所には// MARK: ~~~~(追記)といった形で記載しています。

① クラスの準備

洗濯プログラムを管理するControllerクラスと洗濯機本体である
WashingMachineクラスを用意します。

// クラス「コントローラー」
class Controller {}

// クラス「洗濯機」
class WashingMachine {}

② 「洗濯機」クラスの下準備

// クラス「洗濯機」
class WashingMachine {
    // 洗濯終了フラグ
    var shouldStopWashing = false
    // 洗濯メソッド → 洗濯終了フラグがtrueになるまで洗濯し続ける
    func washing() {
        while !shouldStopWashing {
            print("🧺してます。")
        }
    }
}

③「コントローラー」クラスの下準備

// クラス「コントローラー」
class Controller {
    // 洗濯機をインスタンス化
    let washingMachine = WashingMachine()
    // 洗濯開始メソッド
    func startWashing() {
        print("洗濯開始!!")
        // 洗濯処理の委任
        washingMachine.washing()
    }
    // 告知音
    func notificationSound() {
        print(" ♪ ♪ ♪ ")
    }
}

この時点で「コントローラー」クラスをインスタンス化してstartWashingメソッドを
実行すると、永遠に洗濯が終わらないことになってしまう。

④洗濯開始を通知する処理

ここからデリゲートパターンを実装していくのですが、まずしっかりと整理をしましょう。
筆者はここで頭の整理ができていないまま実装して苦戦しました。

構図の整理

まず通知内容。求められているのは「洗濯を終了したこと」を通知してもらうことです。
そして、どちらが「通知の送り側」で「通知の受け取り側」なのか。今回の例題では

  • 通知を送る側 → 「洗濯機」
  • 通知を受け取る側 → 「コントローラー」
    最初に洗濯の指示を出す「コントローラー」を送り手側と勘違いしてしまわないように
注意しましょう。
    これは処理を委任しているのがコントローラーだと言う事です。
    スクリーンショット 2023-02-14 10.00.44.png

通知を送る処理

❶ まずは通知を管理するデリゲートプロトコルを準備します。
  プロトコル名は通知を受け取るクラス名にDelegateとつけるのが一般的です。

❷ 次にメソッド名はどんな時に通知を送るメソッドなのかを具体的にすると
 
より混乱を防げます。今回は「洗濯機の洗濯が終了した時」なので
 washingMachineDidEndWashingということになります。

上記のルールに従わないで命名してもプログラム上は問題ありませんが、

複数人での開発ではこのようにしてあげるとわかりやすいと思われます。

❸ 引数が存在する場合は処理を委譲されている側に定義するのが原則です。

❹ デリゲートパターン用のプロパティを定義します。

washingメソッド内に❸で定義したwashingTimeを追記して
  洗濯時間をカウントできる様にします。

❻ 洗濯時間の経過がわかるようにプリント文に追記します。

❼ 委任した処理の最後に通知処理を記述します。記述方法はdelegate?.
 続けてデリゲートメソッドを記述します。

❶❷

// ❶洗濯を通知するプロトコル
protocol WashingMachineDelegate {
    // ❷通知に使用するデリゲートメソッド
    func washingMachineDidEndWashing(washingTime: Int)
}

❸❹❺❻❼

// クラス「洗濯機」→ 処理の委任先、通知を送る側
class WashingMachine {
    // 洗濯終了フラグ
    var shouldStopWashing = false
    // MARK: ❸洗濯時間 → デリゲートメソッドの引数(追記)
    var washingTime = 0 // 単位は×10分
    // MARK: ❹デリゲートパターン用のプロパティ
    var delegate: WashingMachineDelegate?
    // 洗濯メソッド → 洗濯終了フラグがtrueになるまで洗濯し続ける
    func washing() {
        while !shouldStopWashing {
            // MARK: ❺洗濯時間を毎回プラス1にする (追記)
            washingTime += 1
            // MARK: ❻洗濯時間をプリント文に反映 (追記)
            print("🧺してます、\(washingTime)0分経過。")
            // MARK: ❼洗濯が終了した時に使うデリゲートメソッド(追記)
            delegate?.washingMachineDidEndWashing(washingTime: washingTime)
        }
    }
}

⑤ 通知を受け取る側の処理

controllerクラスにデリゲートプロトコルを準拠させます。
するとコンパイルエラーが発生します。
これは「プロトコルの1-5 準拠の約束事」で説明した「同じメソッドを定義する」
が守られていないからです。

これを回避するためにcontrollerクラス内にデリゲートメソッドを定義します。
定義するとコンパイルエラーは解除されます。

// クラス「コントローラー」 → 処理の委任元、通知の受け取る側
class Controller: WashingMachineDelegate { // MARK: デリゲートプロトコルを準拠(追記)
    // 洗濯機をインスタンス化
    let washingMachine = WashingMachine()
    // 洗濯開始メソッド
    func startWashing() {
        print("洗濯開始!!")
        washingMachine.washing()
    }
    // MARK: デリゲートメソッドを定義 (追記)
    func washingMachineDidEndWashing(washingTime: Int) {}
    // 告知音
    func notificationSound() {
        print(" ♪ ♪ ♪ ")
    }
}

⑥ 洗濯を終了させる処理

washingMachineDidEndWashingメソッド内に洗濯を終了させる処理を記述します。
今回は洗濯時間の制限時間を3に設定し、30分経過したら終了する処理とします。

❶ 制限時間washingTimeLimitを定義します。

washingMachineDidEndWashing内に制限時間になったらwashingMachineクラスの
 洗濯終了フラグであるshouldStopWashingtrueにして洗濯を終了させます。

❶❷

// クラス「コントローラー」 → 処理の委任元、通知の受け取る側
class Controller: WashingMachineDelegate {
    // 洗濯機をインスタンス化
    let washingMachine = WashingMachine()
    // 洗濯開始メソッド
    func startWashing() {
        print("洗濯開始!!")
        washingMachine.washing()
    }
    // MARK: ❶洗濯の制限時間を定義 (追記)
    var washingTimeLimit = 3 // 単位は×10分
    // MARK: ❷デリゲートメソッドを定義し、終了させる処理を記述 (追記)
    func washingMachineDidEndWashing(washingTime: Int) {
        let isWashingTimeLimit = washingTimeLimit <= washingTime
        if isWashingTimeLimit {
            washingMachine.shouldStopWashing = true
            notificationSound()
        }
    }
    // 告知音
    func notificationSound() {
        print(" ♪ ♪ ♪ ")
    }
}

⑦ デリゲートの登録

最後にデリゲートパターンでは通知を受け取る側は、通知を送る側に対してデリゲートの登録を
メソッドとして行わなければいけません。例えで言うと通信開始の合図のようなイメージです。

今回で言うと下記のようになります。

  • 通知を受け取る側 → Controllerクラス
  • 通知を送る側 → WashingMachineクラス

記述は以下のようになります。
❶名称は通知を知らせるデリゲートメソッドと言うことでregisterDelegate
 としています。 selfは自分自身であるControllerクラスを指します。
❷受診を開始を実行するために今回の例題ではstartWashingメソッドの
 処理ブロックの1行目にregisterDelegateを記述します。

// クラス「コントローラー」 → 処理の委任元、通知の受け取る側
class Controller: WashingMachineDelegate {
    // 洗濯機をインスタンス化
    let washingMachine = WashingMachine()
    // 洗濯開始メソッド
    func startWashing() {
        // MARK: ❷デリゲートの登録メソッド実行 (追記)
        registerDelegate()
        print("洗濯開始!!")
        // 洗濯処理の委任
        washingMachine.washing()
    }
    // 洗濯の制限時間を定義
    var washingTimeLimit = 3 // 単位は×10分
    // デリゲートメソッドを定義し、終了させる処理を記述
    func washingMachineDidEndWashing(washingTime: Int) {
        let isWashingTimeLimit = washingTimeLimit <= washingTime
        if isWashingTimeLimit {
            washingMachine.shouldStopWashing = true
            notificationSound()
        }
    }
    // MARK: ❶デリゲートの登録メソッド (追記)
    func registerDelegate() {
        washingMachine.delegate = self
    }
    // 告知音
    func notificationSound() {
        print(" ♪ ♪ ♪ ")
    }
}

⑧ 完成、実行して確認

完成です。コントローラークラスをインスタンス化し、startWashingメソッドを実行
してみます。

let controller = Controller()
controller.startWashing()

下の画像の様にログが出力すれば実装完了です。
スクリーンショット 2023-02-14 1.40.32.png

ソースコード全体

// 洗濯を通知するプロトコル
protocol WashingMachineDelegate {
    // 通知に使用するデリゲートメソッド
    func washingMachineDidEndWashing(washingTime: Int)
}

// クラス「コントローラー」 → 処理の委任元、通知の受け取る側
class Controller: WashingMachineDelegate {
    // 洗濯機をインスタンス化
    let washingMachine = WashingMachine()
    // 洗濯開始メソッド
    func startWashing() {
        // デリゲートの登録メソッド実行
        registerDelegate()
        print("洗濯開始!!")
        // 洗濯処理の委任
        washingMachine.washing()
    }
    // 洗濯の制限時間を定義
    var washingTimeLimit = 3 // 単位は×10分
    // デリゲートメソッドを定義し、終了させる処理を記述
    func washingMachineDidEndWashing(washingTime: Int) {
        let isWashingTimeLimit = washingTimeLimit <= washingTime
        if isWashingTimeLimit {
            washingMachine.shouldStopWashing = true
            notificationSound()
        }
    }
    // デリゲートの登録メソッド 
    func registerDelegate() {
        washingMachine.delegate = self
    }
    // 告知音
    func notificationSound() {
        print(" ♪ ♪ ♪ ")
    }
}
// クラス「洗濯機」→ 処理の委任先、通知を送る側
class WashingMachine {
    // 洗濯終了フラグ
    var shouldStopWashing = false
    // 洗濯時間 → デリゲートメソッドの引数
    var washingTime = 0 // 単位は×10分
    // デリゲートパターン用のプロパティ
    var delegate: WashingMachineDelegate?
    // 洗濯メソッド → 洗濯終了フラグがtrueになるまで洗濯し続ける
    func washing() {
        while !shouldStopWashing {
            washingTime += 1
            print("🧺してます、\(washingTime)0分経過。")
            // 洗濯が終了した時に使うデリゲートメソッド
            delegate?.washingMachineDidEndWashing(washingTime: washingTime)
        }
    }
}

let controller = Controller()
controller.startWashing()

終わりに

初学者にとって理解が難しいデリゲートパターンについて解説してみました。
記事作成にあたって調べていく中で、今回の事例以外にも通知パターンは他にも存在している事も
知りました。また、デリゲートパターンについても様々な状況でパターンが違うことも知りました。
気になる方は是非調べてみてください。

この記事が初学者の方々の学習において、少しでもお役にたてれば幸いです。

参考著書

<増補改訂第3版> SWIFT実践入門

参考記事

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

4
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
7