Swiftの@MainActorをUIスレッドの所有権として見る

SwiftUIやiOSアプリを書いていると、@MainActor がよく出てきます。

C++でGUIを書いたことがあるなら、「UIはメインスレッドで触る」という制約に近いものとして見ると入りやすいです。

Swift公式のConcurrencyドキュメントでは、MainActorはメインスレッド上で実行されるglobal actorとして説明されています。

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#The-Main-Actor

UI状態をMainActorに寄せる

SwiftUIのViewModelでは、型ごと @MainActor にする形をよく見ます。

@MainActor
final class UserViewModel: ObservableObject {
    @Published var name = ""

    func load() async {
        name = await fetchName()
    }
}

この型のメソッドやプロパティアクセスはMainActor上で扱われます。

UIへ反映する状態を持つなら、この形が分かりやすいです。

非同期処理とUI更新

非同期処理の結果をUIへ反映する場合、MainActorをまたぐ必要があります。

func load() {
    Task {
        let name = await fetchName()
        await MainActor.run {
            self.name = name
        }
    }
}

ただ、ViewModel自体を @MainActor にしているなら、もっと単純に書けることがあります。

@MainActor
func load() async {
    name = await fetchName()
}

どちらにするかは設計次第ですが、UI状態を持つ型はMainActorに寄せる方が追いやすいです。

C++のUIスレッド制約に近い

C++でQtやWin32などを触ると、UIオブジェクトは決まったスレッドで触る必要があります。

Swiftの @MainActor も、UI更新をメイン実行コンテキストへ寄せるための印として見られます。

ただし、Swiftでは型や関数に注釈として書けるので、コンパイラがチェックに参加してくれます。

何でもMainActorにしない

UI状態を持つViewModelをMainActorにするのは自然です。

ただ、重い計算やI/OまでMainActorに寄せるとUIが詰まります。

@MainActor
func calculateLargeData() {
    // 重い処理をここに入れるのは避けたい
}

重い処理は別の非同期関数やactorに分け、結果だけMainActorへ戻す方が安全です。

まとめ

@MainActor は、SwiftのUI状態を扱うときの重要な印です。

  • MainActorはメインスレッド上で実行されるglobal actor
  • UI状態を持つViewModelは @MainActor にしやすい
  • 非同期処理の結果をUIへ戻すときに使う
  • 重い処理までMainActorに置かない

C++の「UIはメインスレッドで触る」を、Swiftでは型や関数に明示できる、と見ると理解しやすいです。

関連記事

参考