たぬきすのアプリ開発日記

アプリ開発の備忘録兼日記

【Swift】Delegateの使い方について

どうも。たぬきすです。

皆さんはdelegateというのをご存知ですか?

簡単に言えばデザインパターンの一種で、クラスの処理の一部を他のクラスに任せることができるので、任せる相手に応じて実行する処理を自由に変えることができるという特徴があります。

delegateの詳しい解説はネット上に数多く存在するので、知っているという人は多いのではないでしょうか。

delegateとは何か詳しく知りたい人は、私が参考にした記事のリンクを貼ったのでよければ参考にしてください。

SwiftにおけるDelegateとは何か、なぜ使うのか - Qiita

  ここでは、delegateのことは知っているけど使いどころがわからないというひとに向けて、私がdelegateを実装して便利だったケースを紹介します。参考になれば嬉しいです。

実装方法

ケースを紹介する前に実装方法についてまとめておこうとおもいます。下は委託する側とされる側のクラスのコードです。

protocol DelegateSampleDelegate: AnyObject {
    func sayHello(str: String)
}

class DelegateSample {
    
    // 委託される側を保持する
    weak var delegate: DelegateSampleDelegate?
    
    func sayHello(){
        let str = "Hello"
        
        // ここで処理を委託する
        delegate?.sayHello(str: str)
    }
}

class Main: DelegateSampleDelegate {
    
    private var delegateSample = DelegateSample()
    
    func main() {
        // delegateSampleに委託させる
        delegateSample.delegate = self
        delegateSample.sayHello()
    }
    
    // ここで委託された処理を実行する
    func sayHello(str: String) {
        print(str)
    }
}

let main = Main()
main.main()

上の例でいうと「DelegateSample」が委託する側,「Main」が委託される側になります。
委託する側のクラスに必要なのは

  • プロトコルを用意し、デリゲートメソッドを定義する
  • 委託される側を保持しておくためのメンバ変数を用意しておく
  • 処理を委託したいタイミングにデリゲートメソッドを呼ぶ

委託される側のクラスに必要なのは、

  • 委託する側用に用意したプロトコルに準拠する(クラスの継承と同じ要領)
  • 委託する側に委託される側を保持させる
  • プロトコルで定義されているデリゲートメソッドを用意する

になります。 ちなみに、委託する側のdelegate変数はweakにしておくことをおすすめします。 前にビューコントローラに処理を委託させたときにweakをつけなかったことが原因でコントローラが破棄されず、メモリリークを起こした経験があります。

今回はシンプルなデリゲート実装を例に挙げました。 しかし、この例だと「DelegateSampleのsayHelloメソッドに戻り値を用意するのとどう違うんだ?」と疑問に思うと思いますが、実は関数だけでは対応が難しい場面があります。 私の経験でいうと

  • DelegateSampleでイベントが起き、その内容をMainに伝えたいとき
  • DelegateSampleで非同期処理を行い、結果をMainに伝えたいとき

がそれに該当しました。

具体的にいうと、前者は定期実行を行うクラスで、後者はWebAPIなどの非同期処理を行うクラスです。

定期実行を行うクラス

下がそのサンプルになります

protocol SampleServiceDelegate: AnyObject {
    func countUpdate(count: Int)
}

class SampleService {
    
    // 処理を任せる側のdelegateを保持する
    weak var delegate: SampleServiceDelegate?
    private var timer = Timer()
    private var count = 0
    
    func start(timeInterval: Double) {
        timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(update), userInfo: nil, repeats: true)
    }
    
    func stop() {
        timer.invalidate()
    }
    
    // 指定したtimeInterval秒ごとに呼び出される
    @objc func update(){
        count += 1
        
        // ここで処理を委託する
        delegate?.countUpdate(count: count)
    }
    
}

class Main: SampleServiceDelegate {
    
    private var sampleService = SampleService()
    
    func main() {
        sampleService.delegate = self
        sampleService.start(timeInterval: 5.0)
    }
    
    // 委託された処理を実行する
    func countUpdate(count: Int) {
        print(count)
    }
    
}

let main = Main()
main.main()

上の例では、timeIntervalで指定した時間ごとにカウントをするという定期実行をするクラス「SampleService」を用意しました。
このとき、カウントが更新されるたびにカウント数をMainで使用したいとします。
上の例のMainクラスはSampleServiceから処理を委託されているので、カウントが更新されるごとにカウント数を受け取ることができ、Mainクラスの中で更新後の処理をすることができます。
ここで、delegateを使わずにカウントを取得しようとしたとき、どのように実装すれば同じことができるでしょうか。
私がパッと思いついたのは

  • カウントが更新された後に行いたい処理を「SampleService」クラスの「update」メソッド内に記述する
  • 「Main」クラスで定期的に「SampleService」クラスの「count」の値を取りにいかせる

の二つです。しかし、この二つの実装は問題があり、前者では「SampleService」クラスの汎用性がなくなりますし、後者では、Mainに実装する機能が増える上に更新されたタイミングでカウントを使用することができません。
delegateを使うおかげで、「SampleService」クラスが汎用性を保ったまま他のクラスで「SampleService」からのイベントを簡単に取得することができるわけです。

非同期処理を行うクラス

下がそのサンプルになります

protocol SampleAsyncDelegate: AnyObject {
    func result(result: Int)
}

class SampleAsync {
    
    // 処理を任せる側のdelegateを保持する
    weak var delegate: SampleAsyncDelegate?
    
    func calcAddAsync(num1: Int, num2: Int) {
        
        DispatchQueue.global().async {
            let result = num1 + num2

            // ここで処理を委託する
            self.delegate?.result(result: result)
        }
        
    }
    
}

class Main: SampleAsyncDelegate {
    
    private var sampleAsync = SampleAsync()
    
    func main() {
        sampleAsync.delegate = self
        sampleAsync.calcAddAsync(num1: 1, num2: 2)
    }
    
    // 委託された処理を実行する
    func result(result: Int) {
        print(result)
    }
    
}

let main = Main()
main.main()

上の例では、非同期処理を伴う「SampleAsync」クラスの「calcAddAsync」メソッドを「Main」クラスで呼び出し、処理結果をdelegateメソッドで受け取るという処理をしています。
できれば「calcAddAsync」メソッドの戻り値で処理結果を返したいところですが、非同期の処理が絡む場合メソッド実行中に処理結果を受け取ることはできません。
ここで、delegateを利用することで非同期処理が終わったタイミングで「Main」クラスに実行結果を返すことができます。
「このクラスのメソッドでどうしても非同期処理をしなければいけないけど処理結果は別クラスで使用したい」という場面に私は結構多く遭遇するのですが、delegateで簡単に解決できるので私はこの方法をよく使っています。
解決方法はこればかりではないみたいなので、もっとよい方法があるかもしれませんが。

あとがき

以上が私がdelegateを使ってみて便利だった実装でした。
極端な話、動くプログラムを作るというだけならdelegateを利用しなくても実装はできます。
ただ、クラスの機能を明確にし、それぞれが独立した汎用性の高いクラスを作ろうとしたときdelegateのようなデザインパターンが必要になってきます。
デザインパターンはよりよい設計を目指し続けた先人たちが編み出してきた、工夫の結晶なんだなあとデザインパターンを利用しているとつくづく思います。
今回紹介した実装はほんの一例でしかありませんが、参考になれば幸いです。