SwiftUI ビューと@MainActor
開発者の数が増えるにつれて、Swift 6 の登場に備えて厳密な並行性チェックを有効にする人が増えています。受け取った警告やエラーの一部は SwiftUI ビューに関連しており、多くは開発者が@MainActor
を正しく理解または使用していないことに起因しています。この記事では、@MainActor
の意味と、SwiftUI ビュー内で@MainActor
を適用するためのヒントと考慮事項について説明します。
この原文は私のブログ Fatbobman's Blog に掲載されています。Swift、SwiftUI、Core Data、SwiftData に関する最新のアップデートや優れた記事をお見逃しなく。Fatbobman's Swift Weekly に登録して、毎週の洞察と貴重なコンテンツを直接メールボックスにお届けします。
私の PasteButton が動かなくなった
最近、私の Discord コミュニティで、厳密な並行性チェックを有効にした後、PasteButton
に対してコンパイラが次のエラーを投げたと友人が報告しました。
同期的非分離コンテキストで main actor-isolated 初期化子'init(payloadType:onPast:)'を呼び出す
PasteButton
の宣言を調べた後、彼にビューコードをbody
の外に置いたかどうかを尋ねました。肯定的な回答を受け取った後、PasteButton
の変数宣言に@MainActor
を追加するようアドバイスし、それで問題が解決しました。
@MainActor public struct PasteButton : View {
@MainActor public init(supportedContentTypes: [UTType], payloadAction: @escaping ([NSItemProvider]) -> Void)
@MainActor public init<T>(payloadType: T.Type, onPaste: @escaping ([T]) -> Void) where T : Transferable
public typealias Body = some View
}
では、問題は最初どこにあったのでしょうか?@MainActor
を追加することでなぜ解決したのでしょうか?
@MainActor とは何か
Swift の並行性モデルでは、actor
は並行コードを安全かつ理解しやすく書くための方法を提供します。actor
はクラスに似ていますが、並行環境でのデータ競合と同期問題を解決するために特別に設計されています。
actor Counter {
private var value = 0
func increment() {
value += 1
}
func getValue() async -> Int {
return value
}
}
let counter = Counter()
Task {
await counter.increment()
}
actor
の魔法は、データ競合を防ぐためにアクセスをシリアライズする能力にあります。これにより、並行操作のための明確で安全な道が提供されます。しかし、この隔離は特定のactor
インスタンスに局所化されます。Swift は、隔離をより広く拡張するためにGlobalActor
の概念を導入しました。
GlobalActor
を使用すると、異なるモジュール間でコードに注釈を付けて、これらの操作が同じシリアルキューで実行されることを保証できます。これにより、操作の原子性と一貫性が維持されます。
@globalActor actor MyActor: GlobalActor
{
static let shared = MyActor()
}
@MyActor
struct A {
var name: String = "fat"
}
class B {
var age: Int = 10
}
@MyActor
func printInfo() {
let a = A()
let b = B()
print(a.name, b.age)
}
@MainActor
は Swift によって定義された特別なGlobalActor
です。その役割は、@MainActor
で注釈されたすべてのコードが同じシリアルキューで実行され、これがすべてメインスレッド上で行われることを保証することです。
@globalActor actor MainActor : GlobalActor {
static let shared: MainActor
}
@MainActor
のこの簡潔で強力な機能は、Swift の並行性モデル内で以前DispatchQueue.main.async
に依存していた操作を型安全かつ統合された方法で扱うことを可能にします。これはコードを単純化し、エラー率を低下させるだけでなく、コンパイラの保護を通じて、@MainActor
でマークされたすべての操作がメインスレッド上で安全に実行されることを保証します。
View プロトコルと@MainActor
SwiftUI の世界では、ビューはアプリケーションの状態を画面に宣言的に表示する役割を果たします。これは、ビューがユーザーインターフェースの提示と直接関連しているため、すべてのコードがメインスレッドで実行されるという前提に自然とつながります。
しかし、View プロトコルを詳しく見ると、body
プロパティのみが明示的に@MainActor
でマークされているという詳細が明らかになります。これは、View プロトコルに適合する型が全てメインスレッドで実行されるとは保証されていないことを意味します。body
を超えて、コンパイラは他のプロパティやメソッドがメインスレッドで実行されることを自動的に保証しません。
public protocol View {
associatedtype Body : View
@ViewBuilder @MainActor var body: Self.Body { get }
}
この洞察は、PasteButton
のように、他のコンポーネントとは異なり明示的に@MainActor
でマークされている SwiftUI の公式コンポーネントの使用を理解する上で特に重要です。これは、PasteButton
が@MainActor
でマークされたコンテキスト内で使用する必要があることを示しています。そうでない場合、コンパイラはエラーを報告し、同期的非分離コンテキストで main actor-isolated 初期化子を呼び出すことは許可されないことを示します:
struct PasteButtonDemo: View {
var body: some View {
VStack {
Text("Hello")
button
}
}
var button: some View {
PasteButton(payloadType: String.self) { str in // 同期的非分離コンテキストで main actor-isolated 初期化子'init(payloadType:onPaste:)'を呼び出す
print(str)
}
}
}
この問題を解決するためには、単にbutton
変数に@MainActor
をマークすることで、コンパイルをスムーズに通過することができます。これにより、button
が適切なコンテ
キスト内で初期化および使用されることを保証します:
@MainActor
var button: some View {
PasteButton(payloadType: String.self) { str in
print(str)
}
}
ほとんどの SwiftUI コンポーネントは値型であり、Sendable プロトコルに適合していますが、
@MainActor
で明示的にマークされていないため、PasteButton
が直面する特定の問題は発生しません。
この変更は、SwiftUI ビューで@MainActor
を使用する重要性を強調し、また、ビューに関連するすべてのコードがデフォルトでメインスレッド上で実行されるわけではないことを開発者に思い出させるものです。
@MainActor をビューに適用する
一部の読者は、PasteButtonDemo
ビュータイプ全体に直接@MainActor
を注釈付けすることで問題を根本的に解決できるのではないかと疑問に思うかもしれません。
確かに、PasteButtonDemo
ビュー全体に@MainActor
を注釈付けすると、問題が解決されます。@MainActor
で注釈された場合、Swift コンパイラはビュー内のすべてのプロパティとメソッドがメインスレッドで実行されると想定します。これにより、button
に別の注釈を付ける必要がなくなります。
@MainActor
struct PasteButtonDemo: View {
var body: some View {
...
}
var button: some View {
PasteButton(payloadType: String.self) { str in
...
}
}
}
このアプローチには他の利点もあります。たとえば、Observation
フレームワークを使用してオブザーバブルオブジェクトを構築する場合、その状態更新がメインスレッドで発生することを確実にするために、オブザーバブルオブジェクト自体に@MainActor
を注釈付けすることができます:
@MainActor
@Observable
class Model {
var name = "fat"
var age = 10
}
しかし、公式ドキュメントに推奨されているように、このオブザーバブルオブジェクトインスタンスをビュー内で@State
として宣言しようとすると、コンパイラ警告が発生します。これは Swift 6 ではエラーとみなされます。
struct DemoView: View {
@State var model = Model() // 非分離コンテキストでの main actor-isolated デフォルト値;これは Swift 6 ではエラーです
var body: some View {
NameView(model: model)
}
}
struct NameView: View {
let model: Model
var body: some View {
Text(model.name)
}
}
この問題は、デフォルトではビューの実装が@MainActor
で注釈されていないために発生します。@MainActor
で注釈された型を直接宣言することができません。DemoView
が@MainActor
で注釈された場合、上記の問題は解決されます。
プロセスをさらに簡素化するために、@MainActor
で注釈されたプロトコルを定義し、このプロトコルに適合する任意のビューが自動的にメインスレッド実行
環境を継承することもできます:
@MainActor
protocol MainActorView: View {}
このプロトコルを実装する任意のビューは、すべての操作がメインスレッドで実行されることを保証します:
struct AsyncDemoView: MainActorView {
var body: some View {
Text("abc")
.task {
await do something()
}
}
func doSomething() async {
print(Thread.isMainThread) // true
}
}
ビュータイプに@MainActor
を注釈付けすることは良い解決策のように思えますが、ビュー内で宣言されたすべての非同期メソッドがメインスレッドで実行される必要があります。これは常に望ましいわけではありません。たとえば:
@MainActor
struct AsyncDemoView: View {
var body: some View {
Text("abc")
.task {
await doSomething()
}
}
func doSomething() async {
print(Thread.isMainThread) // true
}
}
@MainActor
で注釈を付けない場合、必要に応じてプロパティやメソッドに注釈を付けることがより柔軟にできます:
struct AsyncDemoView: View {
var body: some View {
Text("abc")
.task {
await doSomething()
}
}
func doSomething() async {
print(Thread.isMainThread) // false
}
}
したがって、ビュータイプに@MainActor
を注釈付けするかどうかは、具体的なアプリケーションシナリオに依存します。
@StateObject の新たな使用法
Observation フレームワークが新しい標準となるにつれて、@StateObject
の従来の使用法は目立たなくなるかもしれません。しかし、それでも特別な機能を持っており、Observation 時代にも依然として役立つものです。以前に議論したように、@MainActor
でマークされた@Observable
オブジェクトは、ビュー全体も@MainActor
でマークされていない限り、@State
で直接宣言することはできません。しかし、@StateObject
を使用すると、この制限を巧妙に回避できます。
以下の例を考えてみましょう。@MainActor
でマークされた観測可能なオブジェクトを、ビュー全体を@MainActor
でマークすることなくビューに安全に導入することができます:
@MainActor
@Observable
class Model: ObservableObject {
var name = "fat"
var age = 10
}
struct StateObjectDemo: View {
@StateObject var model = Model()
var body: some View {
VStack {
NameView(model: model)
AgeView(model: model)
Button("update age"){
model.age = Int.random(in: 0..<100)
}
}
}
}
この方法の実現可能性は、@StateObject
の一意のローディングメカニズムに由来します。コンストラクタのクロージャがビューが実際にロードされるときにメインスレッドで呼び出されるためです。さらに、@StateObject
のwrappedValue
は@MainActor
で注釈されており、これにより@MainActor
でマークされたObservableObject
プロトコルに適合するタイプを正しく初期化して使用することができます。
@frozen @property
Wrapper public struct StateObject<ObjectType>: SwiftUI.DynamicProperty where ObjectType: Combine.ObservableObject {
@usableFromInline
@frozen internal enum Storage {
case initially(() -> ObjectType)
case object(SwiftUI.ObservedObject<ObjectType>)
}
@usableFromInline
internal var storage: SwiftUI.StateObject<ObjectType>.Storage
@inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType) {
storage = .initially(thunk)
}
@_Concurrency.MainActor(unsafe) public var wrappedValue: ObjectType {
get
}
@_Concurrency.MainActor(unsafe) public var projectedValue: SwiftUI.ObservedObject<ObjectType>.Wrapper {
get
}
public static func _makeProperty<V>(in buffer: inout SwiftUI._DynamicPropertyBuffer, container: SwiftUI._GraphValue<V>, fieldOffset: Swift.Int, inputs: inout SwiftUI._GraphInputs)
}
この方法の主な利点は、観測可能なオブジェクトのライフサイクルの安全性を保証すると同時に、その観測ロジックを Observation フレームワークに基づいて完全に維持することです。これにより、ビューの柔軟性を犠牲にすることなく、@MainActor
でマークされた観測可能なタイプを柔軟に使用できる道が提供されます。Apple がメインスレッド上で@State
を実行するためのより明確な解決策を提供するまでは、このアプローチが実用的かつ効果的な一時的な戦略として機能します。
SwiftUI の現行バージョン(Swift 6 以前)では、開発者がビュー内で
@StateObject
を使用して状態を宣言すると、Swift コンパイラは暗黙的にビュー全体が@MainActor
で注釈されていると推測します。この暗黙的な推論の挙動は、開発者の間で誤解を招くことが容易です。SE-401 提案の公式採用により、Swift 6 からこのような暗黙の推論は許可されなくなります。
結論
厳密な並行性チェックを有効にした後、多くの開発者が混乱や圧倒される感じを抱くかもしれません。一部の開発者はこれらのプロンプトに基づいてコードを変更し、エラーを排除するかもしれません。しかし、新しい並行性モデルをプロジェクトに導入する根本的な目的は、「コンパイラを騙す」ことを超えています。実際には、開発者は Swift の並行性モデルを深く理解し、よりマクロなレベルでコードを再評価することで、より高品質で安全な解決策を発見するべきです。このアプローチは、コードの品質と保守性を高めるだけでなく、開発者が Swift の並行プログラミングの世界をより自信を持って、より容易にナビゲートするのを助けます。
Discussion