【Swift Concurrency】デリゲートを実装するなら？コールバックを安全に保証する方法

Swift で非同期処理（Swift Concurrency）を使うとき、「デリゲートを使ってバックグラウンドで起きたイベントを UI に反映したい」というシチュエーションはよくあります。  
しかし、どのスレッド（アクター）でデリゲートメソッドが呼ばれるかを曖昧にしていると、コンパイラエラーやデータ競合が起きてしまうことも。  
本記事では、中学生にもわかるように、Swift Concurrency 時代のデリゲート実装で「コールバックを保証」する方法を解説します。

---

## 1. シチュエーション例

- GameManager: ゲームの進行を管理するクラス。スコアが変わったときにデリゲートメソッドを呼び出す。  
- ScoreDelegate: スコアが変わったことを画面に伝えるデリゲート。  

Swift Concurrency では、メインアクター（@MainActor）上でしか画面を更新できません。そこで、「デリゲートがどのアクター（スレッド）で呼ばれるか」をしっかり決める必要があります。

---

## 2. デリゲートを @MainActor にするパターン

### 2.1 プロトコルを @MainActor に

```swift
@MainActor
protocol ScoreDelegate {
    func scoreDidUpdate(to newScore: Int)
}
```

こう書くと、「スコアが変わったよ！」というデリゲートメソッドはメインアクターでしか呼べないというルールになります。  
呼び出し側（GameManager）は、メインアクターにジャンプ（`await`）してから呼び出す必要があります。

### 2.2 呼び出し側（GameManager）の例

```swift
class GameManager {
    var delegate: (any ScoreDelegate)?

    func updateScore(to newScore: Int) {
        // バックグラウンドで動いているかも
        // デリゲートを呼ぶときにメインアクターへジャンプ
        Task {
            await delegate?.scoreDidUpdate(to: newScore)
        }
    }
}
```

Task の中でメソッドを呼ぶことで、メインアクターに切り替えてデリゲートメソッドを呼び出します。

### 2.3 デリゲートの実装側（ScoreBoard）

```swift
@MainActor
class ScoreBoard: ScoreDelegate {
    func scoreDidUpdate(to newScore: Int) {
        // ここはメインアクター上
        // UIを更新しても安全
        print("スコアが \(newScore) になりました")
    }
}
```

メインアクターなクラスなので、この中のコードは必ずメインスレッド（メインアクター）で動きます。

---

## 3. フレームワークが非アイソレートなデリゲートの場合

UIKit や他のライブラリのデリゲートは「どのスレッドで呼ばれるか分からない」ケースがよくあります。  
そうすると、プロトコルを @MainActor にできない場合もあります。

### 3.1 自力でメインアクターへジャンプ

```swift
class MyViewController: UIViewController, SomeFrameworkDelegate {
    func frameworkDidUpdateSomething() {
        // ここがバックグラウンドかもしれない
        // 自分でメインアクターへジャンプ
        Task { @MainActor in
            self.updateUI()
        }
    }

    @MainActor
    private func updateUI() {
        // 画面の更新
    }
}
```

フレームワークがいつ・どのスレッドで呼ぶか分からなくても、明示的にメインアクターへ移動すれば UI 更新は安全に行えます。

---

## 4. MainActor.assumeIsolated の例

「本当はメインアクターで呼ばれているのに、コンパイラが証明できない」場合、`MainActor.assumeIsolated` を使って「ここは絶対メインアクターだ！」と強制することもできます。

```swift
class MyViewController: UIViewController, SomeFrameworkDelegate {
    nonisolated func frameworkDidUpdateSomething() {
        // nonisolated なのでアイソレーション外
        // でも実際はメインアクターでしか呼ばれない場合

        MainActor.assumeIsolated {
            // もし本当にメインアクターでなければクラッシュ
            self.updateUI()
        }
    }

    @MainActor
    private func updateUI() {
        // 画面の更新
    }
}
```

`nonisolated` で「アクターアイソレーションを外した」関数にして、`MainActor.assumeIsolated` の中で強制的にメインアクター上と言い張ります。  

実際にメインアクターじゃない状況で呼ばれると**プログラム実行が停止（クラッシュ）し、競合によるユーザーデータ破損を防ぐ**仕組みになっています。
そのため、「本当にメインアクターでしか呼ばれない」と確信している場合 にのみ使うのがポイントです。

---

## 5. なぜコールバック保証が必要？

1. **データ競合を防ぐ**  
   Swift Concurrency はアクターで並行性を管理します。デリゲートがどこで呼ばれるかが曖昧だと、UI 更新でバグやクラッシュが起きやすいです。

2. **コンパイラエラーや警告を回避**  
   Swift 6 の Strict Concurrency では、メインアクター外から同期的に UI をいじるとコンパイルエラーになることも。適切に `@MainActor` や `await`、`MainActor.assumeIsolated` を使うとエラーを防げます。

---

## まとめ

1. **プロトコルを `@MainActor` にする**  
   - メインアクターで呼ばれることが静的に保証される。  

2. **自力でメインアクターへジャンプする**  
   - フレームワークなどでどのスレッドか分からない場合は、明示的にメインアクターへ移動して UI 更新を安全に。  

3. **`MainActor.assumeIsolated` は最終手段**  
   - 実際にはメインアクターで呼ばれているけどコンパイラが証明できない場合に使う。  
   - メインアクター以外ならクラッシュするため、注意が必要。

こうした方法で「デリゲートがどのアクターで呼ばれるか」を明確にすれば、Swift Concurrency でも UI 更新が安全に行われるようになります。デリゲートを実装するなら、コールバックを保証してあげることがとても大切です！