✉️

【iOS】オブジェクト間を疎結合に保つ仕組み

2024/12/24に公開

オブジェクト間の関係を疎結合に保つことで、柔軟性、保守性の高い設計が可能になる。iOSにおいて、その代表的な仕組みが、Delegateである。

Delegateとは

Delegateは、オブジェクト間の関係を疎結合に保ちながら、1対1のコミュニケーションを実現するためのデザインパターンである。Delegateを利用することで、疎結性を保ちながら、動作をカスタマイズできる。この設計思想は、iOSアプリ開発において頻繁に使われる。

Delegateの基本的な考え方

委譲の仕組み

あるオブジェクト(呼び出し元)が、自身の一部の処理を他のオブジェクト(Delegate)に任せる(委譲する)。
Delegateは「どう処理するか」を決める役割を担う。

動作のカスタマイズ

呼び出し元のオブジェクトは具体的な処理を知らなくても良く、Delegateに任せているため、使い方次第で多様な挙動を実現できる。

例えるなら:
店長(呼び出し元)が、アルバイト(Delegate)に「お客様対応をお願い」と任せる感じ。
アルバイトがどんな風に対応するかはアルバイト次第(Delegateの実装次第)。

Delegateの実装(データをロードする機能)

1. Protocolの定義

Delegateに任せたい処理をメソッドとして宣言する。

protocol DataLoaderDelegate: AnyObject {
    func didLoadDataSuccessfully(data: String)
    func didFailToLoadData(error: String)
}

2. Delegateを保持するクラスの作成

class DataLoader {
    // DataLoaderDelegateというプロトコルに準拠したインスタンスを参照する
    weak var delegate: DataLoaderDelegate?

    func loadData() {
        print("Data loading started!")
        
        // 疑似的なデータ取得(成功パターン)
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            let isSuccess = Bool.random() // 成功・失敗をランダムに決定
            if isSuccess {
                self.delegate?.didLoadDataSuccessfully(data: "Sample data loaded successfully!")
            } else {
                self.delegate?.didFailToLoadData(error: "Failed to load data.")
            }
        }
    }
}

3. Delegateを実装するクラスを作成

実際に通知を受け取って処理をするクラスを作る。

class DataViewController: UIViewController, DataLoaderDelegate {
    private var dataLoader: DataLoader?

    override func viewDidLoad() {
        super.viewDidLoad()

        // DataLoaderの初期化とデリゲート設定
        dataLoader = DataLoader()
        dataLoader?.delegate = self
        dataLoader?.loadData()
    }

    // データ読み込み成功時の処理
    func didLoadDataSuccessfully(data: String) {
        print("Data loaded: \(data)")
    }

    // データ読み込み失敗時の処理
    func didFailToLoadData(error: String) {
        print("Error: \(error)")
    }
}

処理の流れ

  1. DataLoader がデータ読み込みタスクを開始する。
  2. 処理が成功または失敗した場合、delegate に通知を送る。
  3. delegate に設定された1つのクラス(例: DataViewController)が通知を受け取り、実際の処理を実行する。

Delegateの利点を掘り下げて理解する

疎結合

Delegateを使うことで、TaskManagerは通知先(具体的な処理)を知らずに済む。これにより、複数のクラス間の結合度が下がり、柔軟なコード設計が可能になる。

カスタマイズ性

Delegateを利用すると、使う側が自由に動作を決められることで、同じ仕組みを使っても異なる挙動を簡単に実現できる。

テストの容易性

テスト時には、DataLoaderDelegate を実装したモッククラスを利用して、DataLoader の通知動作を確認できる。

class MockDataLoaderDelegate: DataLoaderDelegate {
    var successData: String?
    var errorData: String?

    func didLoadDataSuccessfully(data: String) {
        successData = data
    }

    func didFailToLoadData(error: String) {
        errorData = error
    }
}
func testDataLoaderLoadsDataSuccessfully() {
    let dataLoader = DataLoader()
    let mockDataLoaderDelegate = MockDataLoaderDelegate()
    dataLoader.delegate = mockDataLoaderDelegate

    dataLoader.loadData()

    DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
        assert(mockDelegate.successData == "Sample data loaded successfully!")
        print("テスト成功: データ読み込み成功")
    }
}

iOSが提供するフレームワークでの活用シーン

Delegateは、iOSの多くのフレームワークで使われている。

UITableViewDelegate

テーブルビューの行をタップしたときの動作をカスタマイズ。

UICollectionViewDelegate

コレクションビューでのアイテム選択やスクロール操作を処理。

URLSessionDelegate

ネットワーク通信の進捗やエラーを処理。

実践的な応用:UITableViewでのDelegate利用

UIKitではUITableViewDelegateUITableViewDataSourceというDelegateパターンが組み込まれている。

class TableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    let tableView = UITableView()
    let items = ["Apple", "Banana", "Cherry"]

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self
        tableView.frame = view.bounds
        view.addSubview(tableView)
    }

    // DataSourceの実装: セルの数を返す
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    // DataSourceの実装: セルの内容を返す
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
        cell.textLabel?.text = items[indexPath.row]
        return cell
    }

    // Delegateの実装: セルをタップしたときの処理
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("Selected: \(items[indexPath.row])")
    }
}

このコードの動作

  • tableView(_:numberOfRowsInSection:)tableView(_:cellForRowAt:)UITableViewDataSourceのプロトコルメソッド。
  • tableView(_:didSelectRowAt:)UITableViewDelegateのプロトコルメソッド。タップしたアイテムの名前がコンソールに出力される。

Delegateを使う際の注意点

1. 循環参照

Delegateプロパティはweakで宣言し、メモリリークを防ぐ。

2. 複数の通知先が必要な場合

Delegateは1対1の関係を前提としているため、複数の通知先が必要な場合はNotificationCenterやCombineを検討する。

Delegateを使う場合

  • 通知元(Delegateを持つクラス)と通知先(Delegateを実装するクラス)が明確で、単一の通知先だけに処理を委任したい場合。
  • 通知元が通知先の詳細を知らない設計が適切な場合。

NotificationCenterやCombineを使う場合

  • 複数のオブジェクトが同じイベントを受け取って処理する必要がある場合。
  • 特定のオブジェクトに依存せず、通知先を動的に変えたり複数に広げたりしたい場合。

まとめ

Delegateは、オブジェクト間の依存を減らし、1対1の柔軟な通知とカスタマイズ、疎結合を実現する仕組みである。

Discussion