@MainActorは、メインスレッドでのコード実行を保証するSwift言語のグローバルアクターです。Apple Developer, 2024によると、@MainActorはUI操作時のメインスレッドへの切り替えを自動化し、開発者が手動でDispatchQueue.main.asyncを呼び出す必要をなくします。このアノテーションはSwift 5.5のasync/awaitシステムとともに登場しました。
重要ポイント
@MainActorは、アクターの特性とアプリケーションのメインスレッドでの実行保証を組み合わせたSwiftのグローバルアクターです。Swift 5.5でasync/awaitおよび構造化並行処理とともに導入されたSwift並行処理システムの一部です。このアノテーションにより、開発者は手動でのスレッド切り替えを気にする必要がなくなり、UIバグの数が減少します。
Swiftのアクターは、その状態を分離し、1つのスレッドのみが変更できることを保証する参照型です。@MainActorは、その実行基盤がメインスレッドである特別なグローバルアクターです。@MainActorでマークされたコードは、バックグラウンドタスクから呼び出された場合でもメインスレッドで実行されます。
@MainActor以前は、開発者はDispatchQueue.main.asyncを使用して手動でメインスレッドに切り替えていました。これは頻繁なエラーの原因であり、切り替え忘れによるメインスレッド以外でのUI更新クラッシュを引き起こしていました。@MainActorはこの問題を型システムレベルで解決します。
iOSアプリケーションのバグの大部分はUIの安全性の問題、つまりバックグラウンドスレッドからのインターフェース更新に起因します。Appleは@MainActorをSwift Concurrencyに組み込むことで、メインスレッドへの切り替えを自動化しコンパイラでチェック可能にし、実行時エラーのクラス全体を排除しました。
@MainActorの動作原理はSwift Concurrencyの実行システムに基づいています。スレッドが@MainActorでマークされた関数を呼び出すと、スケジューラは現在のエグゼキュータでそれを一時停止し、メインスレッドで再開します。コンパイラは呼び出し境界を追跡し、安全性を保証します。
@MainActorの実行はMainActor.sharedによって管理されます。これはアプリケーションのメインスレッドに関連付けられたエグゼキュータです。非同期関数が@MainActorでマークされている場合、元のタスクがどのスレッドで開始されたかに関係なく、常にこのエグゼキュータで再開されます。
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // 安全に、MainActorがメインスレッドを保証します
}
}
@MainActorでマークされた関数が別の非同期関数を呼び出す場合、デフォルトでアクターコンテキストを継承します。つまり、明示的に指定されない限り、すべてのネストされた呼び出しもメインスレッドで実行されます。コンパイラはこれを追跡し、一貫性のないクロージャを渡そうとするとエラーを発行します。
@MainActorとDispatchQueue.mainの比較は、両方がメインスレッドでのコード実行という同じタスクを解決するものの、新しいメカニズムがより安全で便利とされる理由を理解するのに役立ちます。
@MainActorはコンパイラレベルでのチェックです。安全でないコンテキストから@MainActor関数を呼び出そうとすると、コンパイラが警告またはエラーを発行します。DispatchQueue.main.asyncは実行時の呼び出しであり、コードはコンパイルされますが、バックグラウンドスレッドからUIを更新しようとすると実行時にクラッシュする可能性があります。
DispatchQueue.main.asyncは遅延実行される可能性のあるブロックをキューに追加します。@MainActorはasync/awaitとともに、不要なクロージャを作成せずに直接エグゼキュータを切り替えます。これによりオーバーヘッドが削減され、実行時間がより予測可能になります。
// 古いアプローチ
DispatchQueue.main.async {
self.updateUI()
}
// @MainActorを使用した新しいアプローチ
@MainActor
func updateUI() {
// メインスレッドで実行されます
self.label.text = "更新済み"
}
| 基準 | @MainActor | DispatchQueue.main |
|---|---|---|
| チェック | コンパイラ | 実行時 |
| 構文 | アノテーション(宣言的) | 呼び出し(命令的) |
| オーバーヘッド | 低い(エグゼキュータ切替) | 中程度(クロージャ+キュー) |
| テスタビリティ | 高い(MainActor.sharedを差し替え可能) | 低い(モック化が困難) |
実際のiOSプロジェクトでは、@MainActorはViewModel層、SwiftUIビュー、UIKitコントローラで使用されます。アノテーションは個々のメソッドにも型全体にも適用できます。
クラスを@MainActorでマークすると、すべてのメソッドとプロパティがメインスレッドでのみアクセス可能であることが保証されます。これはSwiftUIビューやObservableObjectクラスに特に便利です。classの前に@MainActorを追加するだけで、すべての@Publishedプロパティが安全に更新されます。
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
スレッド切り替えが手動だった古いUIKitコードを扱う場合、明示的な切り替えにMainActor.runを使用できます。これはコードベース全体を書き換えずにSwift Concurrencyへ段階的に移行するのに便利です。
await MainActor.run {
self.tableView.reloadData()
}
すべての利点にもかかわらず、@MainActorにはアプリケーションアーキテクチャを設計する際に考慮すべきいくつかの制限があります。適用範囲の境界を理解することで、誤った使用を防ぐことができます。
呼び出しチェーン全体が@MainActorでマークされている場合、重い処理はすべてメインスレッドで実行され、UIのフリーズを引き起こします。@MainActorはUI層のみにマークし、ビジネスロジックやネットワークリクエストはバックグラウンドアクターまたはグローバルエグゼキュータに任せることを推奨します。
古いコールバックベースのAPI(async/awaitなしのURLSessionなど)はアクターコンテキストをサポートしていません。統合にはCheckedContinuationを使用したラッパーが必要です。また、@MainActorはperformSelector、target-action、その他の非非同期UIKitパターンと互換性がありません。
@MainActorを使用したアプリケーションのデバッグでは、コンパイラが実行時ではなくビルド時に多くの競合状態を防止するため、レースコンディションの再現が困難です。ただし、これにより誤った安心感が生まれる可能性があります。共有ミュータブルオブジェクト(NSCacheや共有グローバル変数など)が@MainActorでマークされておらず、明示的な同期なしで使用される場合、誤った操作が依然として発生する可能性があります。
@MainActorはテストでの手動スレッド切り替えの必要性をなくすため、UIロジックのテストを大幅に簡素化します。ただし、単体テストやUIテストを作成する際には考慮すべき点があります。
XCTestでは、テスト環境が自動的にメインスレッドエグゼキュータを設定します。テストメソッドがメインスレッドで実行される場合、@MainActor関数の呼び出しに追加の設定は不要で、同じコンテキストで実行されます。バックグラウンドシナリオをテストするには、明示的な優先度とエグゼキュータを指定してTask内でMainActor.runを使用し、バックグラウンドからの呼び出しでコードが正しく動作することを個別に確認します。
一般的なアプローチは@MainActorを使用したViewModelのテストで、非同期操作後に@Publishedプロパティが正しく更新されることを確認します。アクターコンテキストの継承により、テスト内でawaitを呼び出すと、追加のDispatchQueue保証や手動コンテキスト切り替えなしでメインスレッドでの実行が保証され、テストの記述が簡素化されます。
既存のコードをSwift Concurrencyにリファクタリングする際、コンパイラを通じて@MainActorの分離を確認します。@MainActorコンテキストから@MainActorなしの同期メソッドを呼び出すとエラーとしてマークされます。この特性を利用してプロジェクトを段階的にasync/awaitに移行します。ViewModel層を@MainActorとしてマークすると、コンパイラがバックグラウンドアクターに移動する必要がある安全でない呼び出しをすべて強調表示します。
@MainActor依存関係のモックを作成する際、戻り値の型を持つ非同期関数を宣言するasyncメソッドを持つプロトコルを使用します。これにより、アクターの分離を壊すことなくネットワークサービス、データベース、その他の外部依存関係を置き換えることができます。コンパイラはモックがすべての分離要件を実装していることを確認し、バックグラウンドテストスレッドから@MainActorコードへの偶発的なアクセスを防ぎます。
@MainActorコードを同期的にテストする場合、XCTestExpectationを使用して非同期操作の完了を待ちます。テストで期待値を設定し、メインスレッドで実行されるクロージャ内でfulfillmentを呼び出します。テストが無限にハングする場合は、メインスレッドでの呼び出しが発生していない可能性が高く、アクターの分離を確認する必要があります。実行コンテキストをデバッグするには、テストコード内にThread.isMainThreadチェックを追加すると便利です。
よくある質問
いいえ、UIを更新するメソッドのみをマークすれば十分です。ただし、クラスにそのようなメソッドが複数ある場合は、クラス全体に@MainActorを追加する方が簡単です。これにより、すべてのメンバーがメインスレッドで実行されることが保証され、コードの保守が簡素化されます。
@MainActorはメインスレッドに結びついたグローバルアクターの特定のインスタンスです。@globalActorは独自のグローバルアクターを作成するためのプロトコルです。たとえば、プロジェクトのアーキテクチャで必要であれば、バックグラウンドスレッドでコードを実行する@BackgroundActorを作成できます。
はい、@MainActorを使用した同期関数もメインスレッドで実行されます。ただし、@MainActorの主な価値はasync/awaitとともに発揮され、非同期関数がDispatchQueue.mainを介した手動切り替えなしで自動的にメインスレッドで再開されます。
Task.cancel()は通常のタスクと同様に@MainActorタスクでも機能します。@MainActorタスクはTask.isCancelledを確認するか、CancellationErrorをスローできます。キャンセル時、メインスレッドはブロックされず、タスクは最も近い一時停止ポイントで実行を停止します。
コンパイラが安全性を保証します。バックグラウンドコンテキストから@MainActor関数を呼び出すと、コンパイラがエラーを指摘します。非同期呼び出しの場合は、呼び出し元コードにawaitをマークするだけでエグゼキュータがメインスレッドに切り替わります。同期呼び出しの場合は、MainActor.runによる明示的な切り替えが必要です。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。