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. なぜコールバック保証が必要?
-
データ競合を防ぐ
Swift Concurrency はアクターで並行性を管理します。デリゲートがどこで呼ばれるかが曖昧だと、UI 更新でバグやクラッシュが起きやすいです。 -
コンパイラエラーや警告を回避
Swift 6 の Strict Concurrency では、メインアクター外から同期的に UI をいじるとコンパイルエラーになることも。適切に@MainActor
やawait
、MainActor.assumeIsolated
を使うとエラーを防げます。
まとめ
-
プロトコルを
@MainActor
にする- メインアクターで呼ばれることが静的に保証される。
-
自力でメインアクターへジャンプする
- フレームワークなどでどのスレッドか分からない場合は、明示的にメインアクターへ移動して UI 更新を安全に。
-
MainActor.assumeIsolated
は最終手段- 実際にはメインアクターで呼ばれているけどコンパイラが証明できない場合に使う。
- メインアクター以外ならクラッシュするため、注意が必要。
こうした方法で「デリゲートがどのアクターで呼ばれるか」を明確にすれば、Swift Concurrency でも UI 更新が安全に行われるようになります。デリゲートを実装するなら、コールバックを保証してあげることがとても大切です!