こんにちは。この記事では、Swift での並行処理(Concurrency)の基本概念と、特に「MainActor.assumeIsolated」と「@preconcurrency」を使って、昔からあるレガシーな API とのギャップを埋める方法について、できるだけ噛み砕いて解説します。
Qiita を読んでいる皆さんは、Swift の基本的な部分はご存知かもしれませんが、今回の内容は並行処理と安全なコード設計のための新しい考え方に触れる内容です。高校生でも理解できるように、専門用語はなるべく噛み砕いて説明します。
1. Swift の並行処理とは?
まず、並行処理とは、たとえばスマホアプリで画面をスムーズに更新しながら、裏でデータの読み込みや計算を行うような、複数の作業を同時進行で実行する仕組みのことです。
Swift では、async/await
という記法や Actor と呼ばれる仕組みによって、どの処理がどのタイミングで動くかを明確にし、データ競合(同じデータを複数の場所から同時に書き換えてしまう問題)を防ぐことができます。
2. MainActor とメインスレッド
メインスレッドは、画面の更新やユーザーからの操作など、UI(ユーザーインターフェース)に関する処理を行う部分です。Swift では、UI に関するコードを MainActor に隔離することで、「このコードは必ずメインスレッドで実行される」という約束を守る仕組みになっています。
たとえば、UIViewController を @MainActor
で宣言すると、その中のメソッドやプロパティアクセスは自動的にメインスレッドで実行されることが保証されます。
3. レガシー API とのギャップ
しかし、ここで問題が発生します。
昔からある(レガシーな) API やプロトコルは、どのスレッドで呼ばれるかが決まっていません。
たとえば、Objective-C 由来の delegate メソッドなどは「どのスレッドで呼ばれるか分からない」とされています。
一方、私たちが作る UIViewController は @MainActor
に隔離されているため、その中のメソッドは必ずメインスレッドで実行されます。
この状態で、「メイン専用のメソッド」が「どのスレッドでも呼ばれるかもしれない」というレガシーなプロトコルの要求に合致させようとすると、コンパイラから下記のような警告が出てしまいます。
Main actor-isolated instance method 'xxx' cannot be used to satisfy nonisolated protocol requirement
4. この問題を解決する方法
この警告を解消するために、主に以下の 2 つの方法が考えられます。
方法 A: nonisolated と MainActor.assumeIsolated を使う
-
nonisolated 宣言
レガシーなプロトコルで要求されるメソッドは、「非隔離(nonisolated)」として実装します。
つまり、このメソッド自体は「どこで呼ばれても大丈夫」と宣言するのです。 -
MainActor.assumeIsolated の利用
しかし実際には、このメソッド内で@MainActor
に隔離された変数などにアクセスしたい場合は、
MainActor.assumeIsolated { … }
を使って「今、ここはメインスレッドで実行されている」という前提でコードを実行します。
※ もし、実際はメインスレッドでなかった場合は、実行時にクラッシュ(エラー)してくれるので、安全性が保たれます。
【サンプルコード部分】
// レガシーなプロトコル(どのスレッドでも呼ばれるかもしれない)
protocol LegacyDelegate: AnyObject {
func fetchData() -> String
}
@MainActor
class MyViewController: UIViewController, LegacyDelegate {
// メインスレッド専用のプロパティ
var message: String = "こんにちは!"
// プロトコル要求のメソッドを nonisolated として実装
nonisolated func fetchData() -> String {
// ここで「今はメインスレッドで実行中」という保証を与える
return MainActor.assumeIsolated {
return self.message
}
}
}
この方法では、プロトコルで求められている非隔離なメソッド実装と、内部で安全にメインスレッドのデータにアクセスする方法を両立させています。
方法 B: @preconcurrency を使う
Swift では、まだ更新されていない古い API に対して「警告は無視して大丈夫」という指示を出す属性 @preconcurrency
があります。
これをプロトコルの準拠部分につけると、「このプロトコルは昔の API 由来なので、実際はちゃんとメインスレッドで呼ばれているはず。警告は出さなくて大丈夫」とコンパイラに伝えることができます。
【サンプルコード部分】
@MainActor
class MyViewController: UIViewController, @preconcurrency LegacyDelegate {
var message: String = "こんにちは!"
// 普通に実装しても大丈夫(警告は出ない)
func fetchData() -> String {
return self.message
}
}
この方法は、プロトコル側が古い API 由来であるため、実際にはメインスレッドで呼ばれることが確実な場合に使います。
ただし、呼び出し元が本当にメインスレッドであることを前提にしているため、注意が必要です。
5. まとめ
-
Swift の並行処理
複数の作業を同時に安全に行うために、async/await
や Actor などの仕組みを使います。 -
MainActor(メインスレッド)
UI 関連の処理は必ずメインスレッドで行うようにし、@MainActor
を使って安全性を保証します。 -
レガシー API との問題
昔からある API では「どのスレッドで呼ばれるか」が明示されていないため、MainActor で隔離されたコードと合わせると警告が出ます。 -
解決方法
-
nonisolated + MainActor.assumeIsolated
→ メソッド自体は非隔離に宣言しつつ、内部でメインスレッドで実行されることを保証する。 -
@preconcurrency
→ 古いプロトコル準拠の警告を抑制し、通常通り実装する。
-
nonisolated + MainActor.assumeIsolated
このような対応方法を理解しておくと、今後 Swift で並行処理を使う際や、レガシーな API と連携するときにも役立ちます。
Swift の新しい仕組みは初めは複雑に感じるかもしれませんが、一度理解してしまえばより安全なコードが書けるようになります。