Swift Concurrencyでは、UI更新を「メインスレッド(MainActor)」に隔離することで、競合やクラッシュを未然に防げます。本記事は@MainActorの基本から実戦、テスト、移行の勘所までを体系的に解説します。設計判断の迷いを減らし、最小コストで安全性と速度を両立するための実務ガイドです。
MainActorとは—「UIはメインスレッド」の約束をコードで保証する
UIスレッドとグローバルアクターの関係(なぜ必要か)
UIフレームワークはスレッドセーフではない前提が多く、UI更新は常にメインで行う必要があります。MainActorは「メイン=UIスレッド」をグローバルアクターとして表現し、型や関数を隔離することで誤用をコンパイル時に抑止します。結果として、DispatchQueue.mainの呼び忘れや並行実行によるまれな不具合を構造的に回避できます。
@MainActorが守ってくれること/守ってくれないこと
@MainActorは、対象メンバーへのアクセスにメイン隔離を強制し、クロスアクター参照にawaitを要求します。一方で、重い処理を自動でバックグラウンドへ移すわけではなく、設計次第ではメイン滞留を招きます。計算の分離やデータ不変化は依然として開発者の責務であり、アクターだけで万能化はできません。
@MainActorの基本文法と適用範囲
型/メソッド/プロパティへの付与と伝播ルール
@MainActorを型につけると、そのインスタンスメンバーは原則メイン隔離となり、呼び出し側はメイン上での実行やawaitを伴うクロスアクター越境を求められます。メソッドやプロパティ単位で付けることもでき、細粒度に制御可能です。静的メンバーや初期化子の扱い、プロトコル適用などの伝播ルールも理解しておくと設計が安定します。
MainActor.runの使い所(最小スコープでUIを触る)
MainActor.runは一時的にメインへ“ホップ”する手段で、重い処理はバックグラウンドで済ませ、UI更新の瞬間だけ最小スコープで切り替えられます。広い範囲に@MainActorを付けずに済むため、性能と安全性のバランスが取りやすいのが利点です。UI境界を短く保つのが、実務でのコツです。
SwiftUI・UIKitでの実戦パターン
ViewModelを@MainActorで隔離するか?非同期処理の分離戦略
ViewModel全体を@MainActorにするとUI一貫性は得やすい反面、ネットワークや整形といった重い処理までメインに集まりがちです。実務では取得・解析は非隔離型で実行し、結果の適用だけをMainActor.runで反映する二段構えが有効です。UI境界を明確化し、責務を分けるほどパフォーマンスは安定します。
Task/Task.detachedとUI更新の安全な組み合わせ
Taskは呼び出し元アクターの文脈を継承し、Task.detachedはアクター非継承で実行されます。UI更新が必要な箇所は、detachedの中では必ずMainActor.runに切り替えるのが基本です。逆に、純計算やI/Oは非メイン側に残し、UIスニペットだけをメインへホップすることで、スムーズな描画と操作感を維持できます。
nonisolated・@MainActor(unsafe)の正しい使い分け
nonisolatedを使って良いケース/危険なケース
nonisolatedはアクター隔離外として呼び出せるAPIを宣言します。純関数や計算量が小さく、内部で隔離状態に触れないユーティリティには有効です。ただし、非同期で共有状態に触れる可能性があるなら不適切です。見かけの利便性に流されず、読み取り専用・副作用なしの要件を満たすか厳密に判断しましょう。
@MainActor(unsafe)が意味することと回避策
@MainActor(unsafe)は「呼び出し側がメインにいるはず」という前提に頼る指定で、静的解析の保護が弱まります。テストやブリッジ用途など限定的にしか使うべきではありません。基本方針はMainActor.runや明示的な@MainActor付与で安全を確保し、unsafeは最後の手段にとどめることです。将来の保守性にも効きます。
クロスアクター参照の基本とコンパイラの指摘に向き合う
awaitが必要になる理由と“エスケープ”の考え方
他アクターに隔離されたメンバーへアクセスするとき、データ競合を避けるためにawaitが要請されます。これは切り替えコストでもあるため、頻繁に跨がない設計が重要です。クロージャにアクター隔離された参照を“持ち出す”場合も注意し、必要に応じて値コピーやDTO化でエスケープを最小化しましょう。
エラー例(UI更新の抜け漏れ、デッドロックの疑似例)
バックグラウンドからUILabelやStateを書き換えると、実行時クラッシュや未定義挙動を招きます。また、メイン上で重い計算を待ち合わせる設計は描画停止の温床です。UIは短く、計算は外で、境界はMainActor.runで明確に。コンパイラのヒントを抑え込みではなく設計改善の手がかりとして活用しましょう。
Sendable・データ競合・不変設計
メイン隔離とバックグラウンド処理の責務分離
UI層は「表示・入力の反映」に限定し、整形や検証、I/Oはバックグラウンドへ逃がすのが基本線です。境界ではコピー可能な値型やイミュータブルなDTOを受け渡し、共有可変状態を避けます。これによりSendable要件を満たしやすくなり、同時実行による微妙なレースを設計段階で摘み取れます。
値型/参照型とSendableの実務的な判断
値型はスレッド間共有に有利ですが、巨大構造体のコピーはコストになり得ます。参照型は共有が容易な分、同期化の設計が不可欠です。まずはデータ転送境界を明確にし、送る側では値型化・最小単位化、受け側で再構築する発想を持つと、MainActorとの親和性が高まり安全な並行化が進みます。
パフォーマンスとスコープ設計
@MainActorの付け過ぎ問題(広すぎる隔離のデメリット)
型全体に@MainActorを付けると、軽微な計算やI/Oまでメインに縛られ、描画遅延の遠因になります。UI境界は狭く、データ処理は別アクターや非隔離型へ退避する方が総合的に速く安定します。まずは「本当にUIか?」と問い直し、境界単位で刻む習慣を持つと、滞留は目に見えて減ります。
最小単位のMainActor.runでボトルネックを回避
状態更新や描画トリガだけをMainActor.runで包み、前後の計算・変換は非メインで完結させます。さらにUIバッチ更新を意識し、複数の細かな反映を一回のホップにまとめると効率的です。頻繁なアクター跨ぎはそれ自体がオーバーヘッドなので、回数を減らす工夫が体感パフォーマンスに直結します。
既存コードの移行ガイド
段階的導入(警告の無害化→局所適用→型単位)
最初はUI境界の明白な箇所から@MainActorやMainActor.runを導入し、ビルド警告を指標に局所的な改善を進めます。影響範囲とリスクを見極めつつ、最終的に安定した型へ拡張するのが安全です。一度に全域へ適用するより、テストとモニタリングを挟む段階移行が失敗を減らします。
レガシーAPIや通知まわりの扱い(NotificationCenterなど)
古いコールバックや通知は呼び出しスレッドが一定せず、UI更新前にメインへ切り替えるガードが要ります。受信ハンドラでは早々にMainActor.runへ跳び、以降の処理を分岐すると安全です。同期APIの貼り替えやブリッジ層の整備を進め、徐々に非同期・アクター前提の設計へ寄せていきましょう。
テスト戦略(XCTest/Swift Testing)
MainActorで隔離されたコードのテスト手順
テストでは@MainActor付きの対象をそのまま呼ぶか、MainActor.runで囲みます。UI依存を避けるため、状態変化を公開APIや監視プロパティで観測し、副作用はスタブ化します。非同期アサーションを活用し、イベントループの前提違いで不安定にならないよう待機とタイムアウトを適切に設定しましょう。
非同期UI更新の検証テクニック(期待値とタイムアウト)
期待されるUI更新は、一定時間内に所定の状態へ至るかで検証します。ポーリングよりもイベント駆動の通知やAsyncSequenceを使うとテストが安定します。複数更新は順序と間隔も検証対象に含め、不要なメイン待ちを避けつつ確信度を高めます。過度なタイムアウトはCIの遅延要因になるため注意です。
よくある落とし穴・アンチパターン
なんでも@MainActorにする/逆に一切付けない
広範囲の@MainActor指定は安全に見えて、実はパフォーマンス劣化を招きます。一方で完全放置は偶発バグの温床です。UI境界を見極め、最小スコープでメインへ、その他は非メインへという二極化が正攻法。境界の曖昧さが不具合源なので、責務の線引きを文章と図で共有しましょう。
Task.detachedの乱用とUI更新の取りこぼし
detachedは便利ですが、アクター文脈を引き継がないためUI更新が紛れ込みやすく、クラッシュやちらつきの原因になります。UIに触る可能性が少しでもあるならMainActor.runで包む、あるいは通常のTaskに寄せる判断が無難です。ログでホップ箇所を可視化し、誤用を早期に発見しましょう。
サンプル:ニュースアプリのViewModel設計(総合例)
データ取得(非UI)→整形→MainActor.update()の流れ
フェッチとデコードは非メインで実行し、表示用の軽量DTOへ整形してからMainActor.runで状態を書き換えます。失敗時も同様に、UI向けのエラー表現へ変換してから反映します。これにより待ち時間中もUIは応答性を保ち、更新瞬間のみメインを使うため、体感速度と安定性が両立します。
SwiftUIのState/ObservableとMainActorの相性
Observableクラスや@StateはUI駆動の仕組みと密接です。公開プロパティの更新はメインで行う前提を守るべく、ViewModelの公開APIを@MainActorにするか、内部でMainActor.runを徹底します。変更通知が連鎖する場面ではバッチ更新で再描画回数を減らし、無駄なレンダリングを抑えましょう。
チェックリスト(導入前/導入後)
導入前に確認する3点(責務分離・データ不変・UI境界)
①重い処理はUI層から分離できているか。②受け渡しデータは値型中心で不変か。③UI更新の境界が関数またはスコープで明示されているか。まずはこの三点を満たす設計に整え、@MainActorやMainActor.runを追加します。道具の前に設計、これが移行の成功率を大きく左右します。
導入後の監視ポイント(ビルド警告・メイン遅延)
クロスアクター警告は設計の匂いを示す信号です。回数や発生箇所を定期的に確認し、不要なメイン跨ぎを減らしましょう。併せてメインスレッドのフレーム時間や入力遅延を計測し、滞留の兆候を早期発見。CIで簡易計測を自動化すれば、劣化をプルリク単位で検出できます。
よくある質問
MainActorと@MainThreadの違いは?
MainActorはSwift Concurrencyの型安全な隔離機構で、コンパイル時に違反を抑止します。@MainThread相当の指定はランタイム前提の注意喚起に留まる場合が多く、静的保証は弱めです。迷ったらMainActorを基準にし、UI更新は明示的にメインへホップする方針を取りましょう。
MainActor.runを毎回書くべき?
いいえ。UI境界だけを最小スコープで包むのが最適です。広い範囲をメインにすると遅延の温床になります。まずは非メインで完結させ、最後にまとめて反映する「ホップ最小化」を意識しましょう。型やAPIに@MainActorを付ける設計も併用すると、誤用がさらに減ります。
nonisolatedを付けたらUIに触れていい?
いいえ。nonisolatedは隔離外から呼べるだけで、UI安全性を保証しません。内部でUIに触れるならMainActorに切り替えるか、そもそもnonisolated指定をやめるべきです。副作用のない小さな計算に限定して使うのが原則です。
SwiftUIならMainActor不要?
不要ではありません。SwiftUIはUI駆動を助けますが、状態を更新するのは依然メイン前提です。非同期処理の結果を適用する瞬間はMainActor.runや@MainActorでガードし、描画の一貫性を保ちましょう。