0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-02-14

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


1. シチュエーション例

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

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


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

2.1 プロトコルを @MainActor

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

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

2.2 呼び出し側(GameManager)の例

class GameManager {
    var delegate: (any ScoreDelegate)?

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

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

2.3 デリゲートの実装側(ScoreBoard)

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

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


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

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

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

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

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

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


4. MainActor.assumeIsolated の例

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

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 をいじるとコンパイルエラーになることも。適切に @MainActorawaitMainActor.assumeIsolated を使うとエラーを防げます。


まとめ

  1. プロトコルを @MainActor にする

    • メインアクターで呼ばれることが静的に保証される。
  2. 自力でメインアクターへジャンプする

    • フレームワークなどでどのスレッドか分からない場合は、明示的にメインアクターへ移動して UI 更新を安全に。
  3. MainActor.assumeIsolated は最終手段

    • 実際にはメインアクターで呼ばれているけどコンパイラが証明できない場合に使う。
    • メインアクター以外ならクラッシュするため、注意が必要。

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

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?