やさしい @MainActor

## はじめに
多くのUIフレームワークがそうであるように、UIKit・SwiftUIといったAppleプラットフォームのUIフレームワークでは、UIの更新処理をメインスレッドで行う必要があります。

そのため開発者は、どのスレッドでコードが動作するのかを注意深く確認しながらUIの更新処理を実装しなければなりません。しかし、それはとても難しいことです。

そのソリューションとなるのが **MainActor** です。この記事では MainActor が解決する課題を明確にし、 MainActor が私たちにもたらしてくれる恩恵についてやさしく理解できるようにします。

::: note
この記事は出てくるSwiftコードは Xcode 15.4 (Swift 5.9）で実行しビルドおよび動作させています。また、その結果を掲載しています。
:::

## MainActor 登場前のUI更新の課題
MainActor 登場までの課題を再確認するために、次のようなUIKitのコードを例にします。`fetchTodo(id:)` 関数はバックグランドスレッドでTODOデータを取得し、そのバックグランドスレッドからコールバックを呼び出して結果を返すようになっています。

このコールバック処理の中でUIの更新処理を行ってしまうと、UIの動作にも影響する可能性があり、XcodeのMain Thread Checkerによりランタイムで警告されます。

```swift:TodoViewController.swift
import UIKit

class TodoView: UIView {
  convenience init(_ todo: Todo) { ... }
}

class TodoViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    fetchTodo(id: 1) { [weak self] result in
      // ここはバックグラウンドスレッド
      let todo = try! result.get()
      self?.view.addSubview(TodoView(todo)) // ⚠️ UIViewController.view must be used from main thread only
      }
    }
  }
}
```

Xcodeでは下図のように表示されます（だれもが１度は見たことがあるでしょう）。

![Main Thread Checkerによる警告](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/3be49174-d7ea-3360-8109-94e98e13de96.png)


この問題を解消するには、次のコードのようにUI更新処理をメインスレッドで行うようにします。

```swift:TodoViewController.swift
  override func viewDidLoad() {
    super.viewDidLoad()
    
    fetchTodo(id: 1) { result in
      // ここはバックグラウンドスレッド
      DispatchQueue.main.async { [weak self] in
        // ここはメインスレッド
        let todo = try! result.get()
        self?.view.addSubview(TodoView(todo)) // 👌
      }
    }
  }
```

これを開発者が正確に行うことはとても難しいことです。

実行しようとしているメソッドのコールバックがどのスレッドで実行されるのかはコードからは把握することができず、ドキュメントやコメントなどを参照しなければなりません。そしてドキュメントに明記されていないケースも少なくありません（Appleのドキュメントでさえ）。

おそらく多くの開発者はUI更新処理をすべて `DispatchQueue.main.async` で囲うようになったのではないでしょうか。

## Swift Concurrencyの登場による非同期処理の変化
Swift 5.5から言語機能としてSwift Concurrencyが導入されました。これまで非同期処理の結果はコールバックにより返ってきていたものが、 `async/await` により同じコードフローの中で直接結果が得られるようになりました。

```swift:Swift Concurrency以前
// 非同期処理の関数（メソッド）は結果を返すためのコールバック関数を引数にとる
func fetchTodo(id: Int, completion: @escaping (Result<Todo, Error>) -> Void) {
  ...
}

fetchTodo(id: 1) { result in
  // コールバックで結果を受け取る
  let todo = try! result.get()
  print(todo)
}
```
```swift:Swift Concurrency以後
// 非同期関数（メソッド）には `async` キーワードがつく
func fetchTodo(id: Int) async throws -> Todo {
  ...
}

// 完了を `await` で待機する
let todo = try! await fetchTodo(id: 1)
print(todo)
```

ただ、非同期関数（メソッド）はどこでも呼び出せるものではありません。非同期処理を担う[Task](https://developer.apple.com/documentation/swift/task)に指定したオペレーションブロックの中か、別の非同期関数・メソッドの中でのみ呼び出すことができます。

```swift
// Taskのオペレーションブロックの中で、非同期関数を呼び出す
Task {
  let todo = try! await fetchTodo(id: 1)
}

// 別の非同期関数の中で、非同期関数を呼び出す
func someAsyncFunction() async {
  let todo = try! await fetchTodo(id: 1)
}
```

## Swift ConcurrencyとUI更新処理
最初に挙げた例をSwift Concurrencyの非同期関数に置き換えると次のようになります。さて、この中のUI更新処理はどのスレッドで行われるのでしょうか。

```swift:TodoViewController.swift
  override func viewDidLoad() {
    super.viewDidLoad()
    
    Task {
      let todo = try! await fetchTodo(id: 1)
      view.addSubview(TodoView(todo)) // ここはメインスレッド？
    }
  }
```

正解は「メインスレッド」です。その理由をコードとコメントで詳しく解説します。

```swift:TodoViewController.swift
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // ⭐️ここはメインスレッド
    Task {
      // このTaskを実行するスレッドはTask初期化の呼び出し元のコンテキストを引き継ぎます。
      // つまり、このTask内の同期処理はメインスレッドで実行されます。

      // 非同期関数`fetchTodo(id:)`はバックグランドスレッドで処理されます。
      // その間このTask内の処理は一時停止し、メインスレッドを明け渡します。
      let todo = try! await fetchTodo(id: 1)
      
      // `fetchTodo(id:)`メソッドが完了するとこのTaskが再開します。
      // このTask内の同期処理はメインスレッドで実行されるので、ここもメインスレッドです。
      view.addSubview(TodoView(todo)) // 👌
    }
  }
}
```

ご理解いただけたでしょうか。しかし、ここで１つ疑問として残るのが `// ⭐️ここはメインスレッド` の部分です。これはどのように保証されるのでしょうか。

UIKitのフレームワークから `viewDidLoad()` メソッドを呼び出される時は必ずメインスレッドで実行されるように実装されていると思いますが、例えばこの `TodoViewController` を継承したクラスから、誤ってバックグランドスレッドで `super.viewDidLoad()` が呼び出されてしまった場合はどうなるのでしょうか？

安心してください。そのようなことは起こらないようにコンパイラが保証してくれます。その保証の仕組みが **MainActor** です。

## MainActor によるメインスレッド保証
`UIViewController` クラスの宣言に `@MainActor` 属性がついています。

```swift:UIKit.UIViewController
@available(iOS 2.0, *)
@MainActor open class UIViewController : UIResponder, NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment {
    open func viewDidLoad()
```

`@MainActor` 属性のついたクラスや構造体は、そのすべてのメソッド・プロパティが MainActor として扱われます。つまり、 MainActor である `UIViewController` の `viewDidLoad()` メソッドも MainActor です。

:::note info
* クラスや構造体に `@MainActor` がついていたとしても、`nonisolated` キーワードのついたメソッドやプロパティは MainActor から除外されます
* 特定のメソッドやプロパティのみに `@MainActor` を付与することもできます
:::

MainActor の関数やメソッドは**メインスレッドで処理されることがコンパイラにより保証**されます。

例に出てきた `UIViewController.viewDidLoad()` メソッドや、 `UIView.addSubview()` メソッドも MainActor なので、メインスレッドで処理されることがコンパイラレベルで保証されます。

:::note
**MainActor** は `actor` の1つで、メインスレッドで実行されることが保証されたシングルトンの特別な `actor` です。 `actor` についての説明はここで割愛しますが、さきほどのコード例で`Task {}` が呼び出し元から引き継ぐのは `actor` です。ここでは `UIViewController.viewDidLoad()` の `actor` である MainActor を引き継いだので、Task内の処理はメインスレッドで実行されることになります。
```swift
    // ⭐️ここはメインスレッド
    Task {
      // このTaskを実行するスレッドはTask初期化の呼び出し元のコンテキストを引き継ぎます。
      // つまり、このTask内の同期処理はメインスレッドで実行されます。
```
:::

ではメインスレッド以外でUIを更新しようとするとどなるかを試してみましょう。

先ほどの `Task {}` は呼び出し元の `actor` である MainActor を引き継いだのでメインスレッドで実行されましたが、 `Task.detached {}` を使用すると、呼び出し元の `actor` を引き継ぎません。つまり今回の例で言うと、MainActor は引き継がれず何らかのバックグラウンドスレッドで実行されるようになりますので、これを使って試してみます。

```swift:TodoViewController.swift
  override func viewDidLoad() {
    super.viewDidLoad()

    // ここはメインスレッド（viewDidLoad()は MainActor であるため）
    Task.detached {
      // このTask内の同期処理はメインスレッドではない
      // 何らかのバックグラウンドスレッドで実行されます

      let todo = try! await fetchTodo(id: 1)      
      self.view.addSubview(TodoView(todo)) // ❌ Expression is 'async' but is not marked with 'await'
    }
  }
```

`Expression is 'async' but is not marked with 'await'` というコンパイルエラーとなりました。 `UIView.addSubview()` はメインスレッドで非同期実行（ `async` ）する必要があるため、このTask内の処理を一時停止して、メインスレッドでの処理が終わるまで待機（ `await` ）しなさい、ということです。

つまり `UIView.addSubview()` を `await` することでエラーが解消します。

```swift:TodoViewController.swift
  override func viewDidLoad() {
    super.viewDidLoad()

    Task.detached {
      let todo = try! await fetchTodo(id: 1)

      // バックグラウンドスレッドで実行されているこのTaskを一時停止し、
      // メインスレッドで非同期実行されるUI更新が終わるのを待機する
      await view.addSubview(TodoView(todo)) // 👌 
    }
  }
```

`async` キーワードがついていない関数（メソッド）なのに、なぜ `await` をつけないとビルドが通らないんだ！？といったケースはこのパターンですね。

## まとめ

ここまでをまとめると次のとおりです。

* 関数（メソッド）を `@MainActor` 属性を付与して定義することで、メインスレッドで実行される関数（メソッド）であることをコンパイラに伝えることができます
* `@MainActor` 属性が付与された関数（メソッド）を、MainActor ではない場所で呼び出すコードは、コンパイラによりチェックされてエラーになります

**MainActor** により、UI更新処理がメインスレッドで行われることをコンパイラが保証してくれるようになりました🎉

これはUIKitやSwiftUIに属するUIに関するクラスやメソッドの多くに `@MainActor` が付与されているため成り立っています。独自に用意するUIに関するクラスやメソッドも、UI更新を伴うようなものは `@MainActor` 属性をつけておきましょう。

:::note warn
最初のコード例にあるような `DispatchQueue` を直接使用した非同期処理の場合、この **MainActor** による恩恵を受けることができないため、注意が必要です。
:::

## 付録（どこまで MainActor になるのか？）

`@MainActor` を付与したクラス（`class`）・構造体（`struct`）・列挙型（`enum`）の以下すべてが **MainActor** となる。クラスの場合、そのサブクラスも同様。 `@MainActor` を付与したプロトコル（`protocol`）に準拠したクラス・構造体・列挙型も同様。

* initializer
* computed property
* method
* static method
* class method（クラスのみ） 

::: note
`SwiftUI.View` プロトコルは Xcode のバージョンによって MainActor の範囲が異なるようなので要注意です。
* Xcode 16以上 ： `View` 自体が MainActor
* Xcode 15以下 ： `View.body` のみが MainActor
:::

https://x.com/treastrain/status/1800531892019884246

https://x.com/the_uhooi/status/1615908644767531008