SwiftのDelegateを初心者がマスターするための9選の手法

SwiftのDelegateパターンを学ぶ初心者向けのイラストSwift
この記事は約24分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

あなたはSwiftのDelegateパターンについて学びたいと思っているのではないでしょうか?

この記事を読めば、SwiftのDelegateパターンを完全にマスターすることができるようになります。

まず、Delegateとは何か、なぜそれが必要なのか、そして実際にどのように実装するのか、そのすべてをこの記事でカバーします。

サンプルコードも豊富に取り揃えているので、理論だけでなく、実践的な学習も可能です。

●SwiftのDelegateとは

Swiftでのプログラミングにおいて、Delegateは非常に重要な役割を持っています。

しかし、Delegateとは一体何でしょうか?

○Delegateパターンの基本

Delegateパターンは、一つのオブジェクトが全ての責務を持つのではなく、その一部の責務を別のオブジェクト、すなわち「Delegate」に委譲するデザインパターンのことを指します。

これにより、オブジェクト間の結合を弱めることができ、再利用や変更が容易になります。

例えば、ユーザーの入力を処理するUI部品(ボタンやテキストフィールドなど)は、具体的なアクション(ボタンが押された際の処理など)をDelegateに委譲することが多いです。

このようにして、UI部品は再利用性を高めることができます。

○SwiftでのDelegateの役割

Swiftにおいても、Delegateは非常に頻繁に使用される概念です。

特にiOSアプリ開発において、UIKitの多くのクラスがDelegateパターンを採用しています。

例えば、UITableViewやUITextFieldなど、ユーザーの操作を取り扱うUI部品は、その操作の詳細な処理をDelegateに委譲することで、柔軟に動作をカスタマイズすることができます。

●Delegateの作り方

SwiftでDelegateを効果的に使用するには、その作り方をしっかりと理解することが必要です。

ここでは、Delegateの基本的な実装方法から始め、データの受け渡しに至るまでの手順を詳細に解説します。

○基本的なDelegateの実装方法

Delegateを実装するには、まずProtocolを定義する必要があります。

Protocolは、どのようなメソッドがDelegateによって実装されるべきかを定義するものです。

これにより、特定のオブジェクトがDelegateとして機能するための「契約」を結ぶことができます。

下記のコードではProtocolを使ってDelegateを定義する基本的な方法を表しています。

この例では、特定のアクションが完了した際に呼ばれるメソッドをProtocolとして定義しています。

// DelegateのProtocolを定義
protocol ActionCompletedDelegate: AnyObject {
    func actionDidComplete()
}

class TaskManager {
    weak var delegate: ActionCompletedDelegate?

    func performTask() {
        // 何らかのタスクの実行
        // ...

        // タスク完了後、Delegateメソッドを呼び出し
        delegate?.actionDidComplete()
    }
}

上記のコードでは、TaskManagerクラスがタスクを実行した後に、DelegateメソッドactionDidCompleteを呼び出しています。

このように、Delegateを使うことで、特定のイベントが発生したときに任意の処理を追加することができます。

□サンプルコード1:Delegateの基本形

下記のサンプルコードは、Delegateを利用して情報を受け取る簡単な例を表しています。

この例では、DataManagerクラスがデータのロードを行い、完了した際にその結果をDelegateを通して伝える仕組みを作っています。

protocol DataManagerDelegate: AnyObject {
    func dataDidLoad(data: [String])
}

class DataManager {
    weak var delegate: DataManagerDelegate?

    func loadData() {
        // 仮のデータをロードする
        let data = ["apple", "banana", "cherry"]

        // データロード完了後、Delegateメソッドを呼び出し
        delegate?.dataDidLoad(data: data)
    }
}

このコードを実行すると、loadDataメソッドが呼ばれた際に、データがロードされ、その後、DelegateメソッドdataDidLoadが呼ばれることになります。

これにより、データが正しくロードされたことを外部のオブジェクトに通知することができます。

□サンプルコード2:Delegateを使ったデータ受け渡し

Delegateの大きな利点の一つは、データの受け渡しを容易にすることができる点です。

下記のサンプルコードでは、SettingsViewControllerという設定画面でユーザーの入力を受け取り、その結果をDelegateを通してMainViewControllerに伝える例を表しています。

protocol SettingsDelegate: AnyObject {
    func didUpdateSettings(value: String)
}

class SettingsViewController: UIViewController {
    weak var delegate: SettingsDelegate?

    @IBAction func saveButtonTapped(_ sender: Any) {
        let inputValue = "Sample Data" // ここでは仮のデータを使用
        delegate?.didUpdateSettings(value: inputValue)
    }
}

class MainViewController: UIViewController, SettingsDelegate {
    func didUpdateSettings(value: String) {
        print("設定が更新されました: \(value)")
    }
}

上記のコードを実行すると、「saveButtonTapped」メソッドが呼ばれた際に、DelegateメソッドdidUpdateSettingsMainViewController内で呼び出され、設定の更新結果がコンソールに表示されることになります。

●Delegateの詳細な使い方

Delegateのパターンを基本的なレベルで理解したら、次はその詳細な使い方について学ぶことが重要です。

Swiftの様々なフレームワークやライブラリは、Delegateを活用してユーザーとのインタラクションを管理しています。

ここでは、具体的にUITableViewUITextFieldのDelegateを使用した例を取り上げます。

○サンプルコード3:UITableViewのDelegateを実装

SwiftにおけるUITableViewは、リスト形式のデータを表示するための主要なコンポーネントの一つです。

このコンポーネントの動作をカスタマイズするためには、DelegateとDataSourceの2つのProtocolを実装する必要があります。

下記のサンプルコードは、UITableViewDelegateUITableViewDataSourceを使って、テーブルビューにデータを表示する基本的な方法を表しています。

この例では、文字列の配列をテーブルビューに表示しています。

import UIKit

class TableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    var items = ["item1", "item2", "item3"]

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = items[indexPath.row]
        return cell
    }
}

このコードでは、UITableViewDataSourceの2つの主要なメソッド、numberOfRowsInSectioncellForRowAtを使って、テーブルビューにデータを提供しています。

具体的には、items配列の内容がテーブルビューに表示されることになります。

○サンプルコード4:UITextFieldのDelegateを活用

UITextFieldは、ユーザーからのテキスト入力を受け付けるUIコンポーネントです。

入力内容の検証や、リターンキーが押されたときの動作など、UITextFieldの動作をカスタマイズするためには、UITextFieldDelegateを実装します。

下記のサンプルコードは、UITextFieldDelegateを活用して、テキストフィールドの内容が変更された際にその内容をコンソールに表示しています。

import UIKit

class TextFieldViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        textField.delegate = self
    }

    func textFieldDidEndEditing(_ textField: UITextField) {
        if let text = textField.text {
            print("入力内容: \(text)")
        }
    }
}

このコードでは、textFieldDidEndEditingメソッドを使用して、テキストフィールドの編集が終了した際に、その内容をコンソールに表示しています。

このように、Delegateを使うことで、UIコンポーネントの各種イベントに応じてカスタムな処理を追加することができます。

●Delegateの詳細な対処法

SwiftのDelegateパターンは、簡単にはじめられるものの、実際の開発の中で様々な問題や挙動の不具合に直面することも少なくありません。

ここでは、Delegateを使用している際のよくある問題やその対処法について詳しく解説していきます。

○サンプルコード5:Delegateメソッドが呼ばれない時の対処法

Delegateメソッドが正常に呼ばれないことは、開発者が直面する一般的な問題の一つです。

主な原因として、Delegateのセットアップが不完全である、またはDelegateの参照が解放されてしまっていることが考えられます。

下記のコードは、Delegateのセットアップが不完全な場合の例です。

import UIKit

protocol SampleDelegate: AnyObject {
    func didTapButton()
}

class SampleViewController: UIViewController {
    weak var delegate: SampleDelegate?

    @IBAction func buttonTapped(_ sender: UIButton) {
        delegate?.didTapButton()
    }
}

class ParentViewController: UIViewController, SampleDelegate {
    let sampleVC = SampleViewController()

    func setup() {
        // ここでDelegateのセットアップを忘れている
        self.present(sampleVC, animated: true, completion: nil)
    }

    func didTapButton() {
        print("ボタンがタップされました。")
    }
}

このコードでは、SampleViewControllerからdidTapButtonメソッドを呼び出そうとしていますが、setupメソッド内でDelegateを設定していないため、メソッドは実行されません。

対処法としては、setupメソッド内でDelegateを適切に設定することです。

具体的には、次のように修正します。

func setup() {
    sampleVC.delegate = self
    self.present(sampleVC, animated: true, completion: nil)
}

この変更により、ボタンがタップされるとdidTapButtonメソッドが正常に呼び出され、”ボタンがタップされました。”というメッセージがコンソールに表示されます。

○サンプルコード6:複数のDelegateを管理する方法

アプリケーションが成長するにつれて、一つのオブジェクトに対して複数のDelegateを持つ必要が生じることもあります。

しかし、SwiftのDelegateパターンは基本的に一つのDelegateしか持つことができません。

この制約を乗り越えるための一つの方法は、複数のDelegateを管理する専用のクラスを作成することです。

下記のコードは、複数のDelegateを管理するためのMultiDelegateクラスを使った例です。

import UIKit

protocol SampleDelegate: AnyObject {
    func didTapButton()
}

class MultiDelegate: SampleDelegate {
    private var delegates: [SampleDelegate] = []

    func addDelegate(delegate: SampleDelegate) {
        delegates.append(delegate)
    }

    func didTapButton() {
        delegates.forEach { $0.didTapButton() }
    }
}

class ViewController: UIViewController {
    let multiDelegate = MultiDelegate()

    func setup() {
        let firstDelegate: SampleDelegate = FirstClass()
        let secondDelegate: SampleDelegate = SecondClass()

        multiDelegate.addDelegate(delegate: firstDelegate)
        multiDelegate.addDelegate(delegate: secondDelegate)

        // 何らかの操作でdidTapButtonを呼び出す
        multiDelegate.didTapButton()
    }
}

class FirstClass: SampleDelegate {
    func didTapButton() {
        print("FirstClassでボタンがタップされました。")
    }
}

class SecondClass: SampleDelegate {
    func didTapButton() {
        print("SecondClassでボタンがタップされました。")
    }
}

このコードでは、MultiDelegateクラスを通じて、複数のDelegateオブジェクトを同時に呼び出すことができます。

didTapButtonメソッドが呼び出されると、登録されている全てのDelegateの同じメソッドが順番に実行されます。

●Delegateの詳細な注意点

SwiftでのDelegateパターンは極めて強力で、多くのiOSアプリケーションの基盤となっています。

しかし、このパターンを正しく使用しないと、不具合や保守性の低下を招く可能性があります。

ここでは、Delegateの使用に関する注意点や命名規則について深く探ることで、より安全にSwiftのDelegateを利用するための知識を得ることができます。

○循環参照を避ける

SwiftにおけるDelegateパターンの一般的な問題点の一つは、循環参照です。

これは、Delegateをstrong referenceとして保持することで発生する可能性があります。

これを防ぐためには、Delegateをweakまたはunownedとして宣言することが推奨されます。

下記のサンプルコードは、循環参照を生む潜在的な危険を表しています。

import UIKit

protocol DangerousDelegate: AnyObject {
    func didTriggerEvent()
}

class DangerousViewController: UIViewController {
    var delegate: DangerousDelegate? // ここでstrong referenceを持ってしまっている
    ...
}

上記のようにstrong referenceを保持してしまうと、DangerousViewControllerとDelegateとの間で循環参照が生じるリスクがあります。

これを避けるためには、次のようにdelegate変数をweakとして宣言します。

weak var delegate: DangerousDelegate?

この修正により、循環参照を回避し、Delegateパターンを安全に使用することができます。

○Delegateの命名規則

SwiftでのDelegateの命名は、一貫性を持たせることが大切です。

ここでは、Delegateの命名に関する基本的なガイドラインを紹介します。

  1. Delegateプロトコルは、関連するクラスや機能の名前に「Delegate」という接尾辞を追加して命名します。
  2. Delegateメソッドは、何が発生したのかを表す動詞を含むようにします。

下記のサンプルコードは、命名規則に従ったDelegateの実装例です。

import UIKit

protocol ButtonTapDelegate: AnyObject {
    func buttonDidTap(sender: UIButton)
}

class ButtonViewController: UIViewController {
    weak var delegate: ButtonTapDelegate?

    @IBAction func buttonTapped(_ sender: UIButton) {
        delegate?.buttonDidTap(sender: sender)
    }
}

このコードでは、ボタンがタップされたことを通知するためのDelegateメソッドを、命名規則に基づいてbuttonDidTapとしています。

このような命名規則を適用することで、コードの読みやすさや保守性が向上します。

●Delegateの詳細なカスタマイズ

DelegateパターンはSwiftで頻繁に使われる概念であり、多くの標準ライブラリでも採用されています。

しかし、場合によっては独自のDelegateを設計する必要が生じることもあります。

ここでは、独自Delegateの作成から非同期処理の管理、エラーハンドリングに至るまで、Delegateの高度なカスタマイズ方法を学んでいきます。

○サンプルコード7:独自Delegateの作成

独自のDelegateを作成する際には、まずプロトコルを定義します。

このコードでは、ユーザーのアクションを取得するためのDelegateを表しています。

この例では、ユーザーが「保存」ボタンをタップした際の動作をDelegateとして定義しています。

protocol UserActionDelegate: AnyObject {
    func didTapSaveButton(data: String)
}

class UserViewController: UIViewController {
    weak var actionDelegate: UserActionDelegate?

    @IBAction func saveButtonTapped(_ sender: UIButton) {
        let data = "ユーザーデータ"
        actionDelegate?.didTapSaveButton(data: data)
    }
}

このコードでは、UserActionDelegateという名前のDelegateを作成し、その中にdidTapSaveButtonというメソッドを定義しています。

UserViewControllerでは、このDelegateを適切に利用して、「保存」ボタンがタップされた際の動作を外部に通知することができます。

○サンプルコード8:Delegateでの非同期処理の管理

非同期処理とDelegateを組み合わせることで、データの読み込みやネットワーク通信などの非同期タスクの完了時にDelegateを通じて結果を伝えることができます。

下記のコードは、非同期的にデータを取得し、その結果をDelegateを使用して伝える方法を表しています。

protocol DataFetchingDelegate: AnyObject {
    func didFetchData(data: String, error: Error?)
}

class DataFetcher {
    weak var fetchDelegate: DataFetchingDelegate?

    func fetchData() {
        // 非同期処理の模擬
        DispatchQueue.global().async {
            let data = "非同期で取得したデータ"
            // 完了後、メインスレッドでDelegateに結果を伝える
            DispatchQueue.main.async {
                self.fetchDelegate?.didFetchData(data: data, error: nil)
            }
        }
    }
}

このコードでは、非同期処理を模擬的にDispatchQueueを使って実装しています。

データの取得が完了すると、didFetchDataメソッドを使って結果をDelegateに通知します。

○サンプルコード9:独自Delegateでのエラーハンドリング

Delegateを使ってエラーハンドリングを行う場合、DelegateメソッドにError型の引数を追加することで、エラー情報を伝えることができます。

下記のコードは、データの取得時にエラーが発生した場合のエラーハンドリングをDelegateを使用して行う方法を表しています。

protocol ErrorHandlerDelegate: AnyObject {
    func didCompleteTask(withError error: Error?)
}

class TaskHandler {
    weak var errorDelegate: ErrorHandlerDelegate?

    func performTask() {
        // タスク実行の模擬
        DispatchQueue.global().async {
            // エラーを模擬的に生成
            let error = NSError(domain: "TaskError", code: 1001, userInfo: nil)
            // 完了後、メインスレッドでDelegateにエラー情報を伝える
            DispatchQueue.main.async {
                self.errorDelegate?.didCompleteTask(withError: error)
            }
        }
    }
}

このコードでは、performTaskメソッドでエラーを模擬的に生成し、そのエラー情報をdidCompleteTaskメソッドを通してDelegateに伝えています。

これにより、タスクの実行結果が成功か失敗かをDelegateを使用して外部に通知することができます。

●応用例

SwiftでのDelegateの利用は基本的な部分だけでなく、さまざまな応用例が考えられます。

ここでは、より高度な使い方や、Delegateを活用した複雑なUIの制御、複数のデータソースの統合などの応用的な内容を解説します。

○サンプルコード10:Delegateを活用した複雑なUIの制御

多機能なアプリケーションを作成する際、異なるUIコンポーネント間でデータの受け渡しや状態の管理を行う必要があります。

下記のコードは、Delegateを用いて複数のUIコンポーネントの動作を調整する例を表しています。

protocol UIStateDelegate: AnyObject {
    func didChangeState(to state: UIState)
}

enum UIState {
    case loading, empty, filled
}

class MainViewController: UIViewController {
    weak var uiStateDelegate: UIStateDelegate?

    func loadData() {
        // データ取得の模擬
        let isDataAvailable = true

        if isDataAvailable {
            uiStateDelegate?.didChangeState(to: .filled)
        } else {
            uiStateDelegate?.didChangeState(to: .empty)
        }
    }
}

このコードでは、アプリケーションのUIの状態を表すUIStateというEnumを使用しています。

MainViewController内でデータをロードする際、データが存在すれば.filled、存在しなければ.emptyという状態に変更し、Delegateを通してその状態変化を通知します。

○サンプルコード11:Delegateを使って複数のデータソースを統合

大規模なアプリケーションでは、複数のデータソースからのデータ取得や、それらのデータを統合する処理が必要となることがあります。

下記のコードは、Delegateを用いて異なるデータソースからのデータを統合する方法を表しています。

protocol DataMergeDelegate: AnyObject {
    func didMergeData(result: [String])
}

class DataMerger {
    weak var mergeDelegate: DataMergeDelegate?

    func mergeData(source1: [String], source2: [String]) {
        let mergedData = source1 + source2
        mergeDelegate?.didMergeData(result: mergedData)
    }
}

class ViewController: UIViewController, DataMergeDelegate {
    func didMergeData(result: [String]) {
        // 統合されたデータを使用する処理
        print(result)
    }

    func fetchDataAndMerge() {
        let dataFromDB = ["DB1", "DB2"]
        let dataFromAPI = ["API1", "API2"]

        let merger = DataMerger()
        merger.mergeDelegate = self
        merger.mergeData(source1: dataFromDB, source2: dataFromAPI)
    }
}

このコードでは、DataMergerクラスが2つのデータソースを統合する役割を持ち、統合が完了したらdidMergeDataを通じて結果をDelegateに伝えます。

ViewControllerではこの結果を受け取り、必要な処理を行います。

まとめ

SwiftにおけるDelegateパターンは、アプリケーション開発の基本的な部分から応用的な部分まで幅広く利用される重要なテクニックです。

本シリーズを通じて、Delegateの基本的な概念から、実際の実装方法、応用例に至るまで詳しく学ぶことができたかと思います。

Swiftでのアプリケーション開発を行う上で、Delegateは避けて通れないテーマとなっています。

この知識をベースに、より高度なアプリケーションの開発に挑戦してみることをおすすめします。

Delegateパターンを適切に活用することで、効率的で柔軟性の高いコードを実現できるでしょう。