SwiftUI.Button の action は @MainActor なのか - @Sendable でないクロージャが actor コンテキストを受け継ぐことについて

## 概要

SwiftUI で開発する際、ViewModel を `@MainActor` にしてそのメソッドを `Button` の `action` パラメータに渡すということがよくあると思います。このとき、 `action` に渡すクロージャの中で `@MainActor` である ViewModel のメソッドが `await` せず同期的に呼べるので `action` 引数は `@MainActor` なのかと思っていたのですが、ある日同じメソッドを引数に直指定すると `@MainActor` を `@MainActor` でない型に渡すなという旨の warning が出てしまいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/e980dd17-7423-4ee1-5bbd-f6438a7130ce.png)

一見、もし `action` が `@MainActor` なら `// 2` のメソッドを直指定するケースで warning が出ないはずだし、 `@MainActor` でなければ `// 1` のクロージャの中でメソッドを呼ぶケースで actor の制約のため `Button(action: { await viewModel.onTapped() })` のように `await` で呼ばなければいけないはずなので説明がつかないように思えます。

同じことは `Button` の `action` だけではなく、例えば `onTapGesture` の `perform` パラメータなど SwiftUI のクロージャを渡す API 全般で発生します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/8a04d63c-cc42-c264-3c05-1b433748fcc2.png)

この理由が気になって考えてみたのでこの記事でまとめます。やや自信のない箇所もあるので、もし間違いを見つけた方はコメント等で教えてください。

記事中の動作検証は Xcode 14 Beta 5 で行っています。

## TL;DR

前提として、 `@Sendable` でないクロージャは定義された箇所の actor コンテキストを受け継ぐ。`SwiftUI.Button` の `action` 引数は `@MainActor` ではないが、

- `SwiftUI.Button` の `action` 引数は `@Sendable` ではない
- `action` にクロージャを渡す場合 `SwiftUI.View` の `body` の中で定義されることになり、かつ `body` は `@MainActor` である

という２つの事実により、 `action` に渡すクロージャをその場で定義した場合は `@MainActor` を受け継ぐため、クロージャの中で `@MainActor` のメソッドを同期的に呼ぶことができる。 `action` 自体が `@MainActor` なわけないのでクロージャをその場で定義せずにメソッドを直指定すると warning が出る。

## `@Sendable` でないクロージャは actor コンテキストを引き継ぐ

まず前提として、 「actor の中でクロージャが定義されたときにそのクロージャが `@Sendable` でない場合は actor のコンテキストを受け継ぐ」ということを理解していきます。[SE-0306 Actors](https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md) からクロージャと actor の関係について引用します。

> A closure that is not `@Sendable` cannot escape the concurrency domain in which it was formed. Therefore, such a closure will be actor-isolated if it is formed within an actor-isolated context. This is useful, for example, when applying sequence algorithms like `forEach` where the provided closure will be called serially ...


> A closure formed within an actor-isolated context is actor-isolated if it is non-`@Sendable`, and non-isolated if it is `@Sendable`.


簡単にまとめると、クロージャが `@Sendable` でない場合は actor コンテキストを受け継ぎ、 `@Sendable` の場合は受け継がないということです [^1] 。例えば `Task.detached` に渡すクロージャは `@Sendable` 、 `Array#forEach` に渡すクロージャは `@Sendable` ではないので以下のように `forEach` のクロージャの中でのみ actor の状態に同期的にアクセスできます。

[^1]: 例外として、 `Task.init` が受け取るクロージャは `@Sendable` であるにも関わらず actor コンテキストを受け継ぎます

```swift
actor MyActor {
    var number: Int = 42
    
    func f(values: [Int]) {
        Task.detached { // @Sendable なクロージャ
            // actor のコンテキスト外なので number プロパティにアクセスするために await が必要
            await print(self.number)
        }
        
        values.forEach { value in // @Sendable でないクロージャ
            // actor のコンテキスト内なので number プロパティに同期的にアクセスできる
            print(value + self.number)
        }
    }
}
```

以下、 `@Sendable` でないクロージャは、 Concurrency ドメインの壁を越えることができず、他のタスクや actor では実行できないので安全に actor のコンテキストを受け継ぐことができることを理解していきます。

### `@escaping` でないクロージャ

`forEach` 渡すクロージャのように `@escaping` でないクロージャについては、必ず同期的に実行されるので actor コンテキストを受け継ぐのは自然です。以下 `// 1` と `// 2` のループは同じように実行されますが、前者の `for` 文の中で実行されるコードは当然 actor コンテキスト内で実行されるので、後者の `forEach` に渡すクロージャも actor コンテキスト内で実行されて問題ないということです。

```swift
actor MyActor {
    var number: Int = 42
    
    func f(values: [Int]) {
        // 1
        for value in values {
            print(value + self.number)
        }
        
        // 2
        values.forEach { value in
            print(value + self.number)
        }
    }
}
```

### `@escaping` なクロージャ

`@escaping` でないクロージャが actor コンテキストを受け継いでも問題ないことは明らかな一方で、 `@escaping` なクロージャが actor コンテキストを受け継いで問題ないのかは気になるところです。 `@escaping` なクロージャの使い道としてよくある

- その場で非同期に実行される
- プロパティに保存されて後で実行される

についてそれぞれ見ていきます。

#### `@escaping` なクロージャが非同期に実行される場合

クロージャが actor のコンテキストを受け継ぐということは actor のもつ状態を変更できたり、状態を同期的に読み取れるということです。そのクロージャが非同期に実行されてしまっては actor にデータ競合を持ち込んでしまうように思えます。例えば下記のようなコードでは `// 1` は `DispatchQueue.global().async` でスレッドを切り替えて非同期に実行されるので、 `// 1` の操作と `// 2` の操作が同時に別のスレッドから実行されてデータ競合が発生する可能性があります。

```swift
struct Executor {
    init(operation: @escaping () -> Void) {
        DispatchQueue.global().async {
            operation()
        }
    }
}

actor MyActor {
    var number: Int = 42
    
    func f() {
        let _ = Executor {
            self.number += 1 // 1
        }
        self.number += 1 // 2
    }
}
```

しかし、実際にはコンパイラが下記のような warning を出してくれてデータ競合の可能性を防げるようになっています。

```swift
struct Executor {
    init(operation: @escaping () -> Void) {
        DispatchQueue.global().async {
            // ❗️ Capture of 'operation' with non-sendable type '() -> Void' in a `@Sendable` closure
            operation()
        }
    }
}
```

`DispatchQueue#async` だけでなく、 `Task.detached` や `DispatchQueue#asyncAfter` のようなメソッドに置き換えても同様の warning が出ます。

現在の Swift では上記の `Task.detached` や  `DispatchQueue#async` のような非同期に処理を実行する API は `@Sendable` なクロージャを受け取るようになっています。そのため、 actor 内で定義した `@Sendable` でないクロージャが最終的に非同期に実行されようとしたところで warning が出るようになっています。コンパイラの warning を無視しない限りはそのクロージャは同期的に実行される必要があり、結果として安全に actor コンテキストを受け継ぐことができるというわけです。

注意点として、上記の warning は Xcode 14 Beta 5 の時点ではこの warning は Strict Concurrency Check を `Complete` に設定しないと出ないようになっています。おそらく、 Swift 6 の時点ではこの warning はデフォルト設定でも error になるのではないかと思っています。 Strict Concurrency Check について詳しくは以下の記事を参照ください。

https://qiita.com/maiyama18/items/7f2a7c0ed4cce9d3af72

#### `@escaping` なクロージャがプロパティに保存されて後で実行される場合

`@escaping` なクロージャはプロパティに保存することができるので、そのクロージャが actor のコンテキストを受け継いでいる場合にプロパティに保存されたクロージャを後から実行しても問題が発生しないか見てみます。例えば以下のコードでは `// 1` の操作と `// 2` の操作が同時に別のスレッドから実行されることがありうるのでデータ競合が発生する可能性があります。

```swift
struct Executor {
    private var operation: () -> Void
    
    init(operation: @escaping () -> Void) {
        self.operation = operation
    }
    
    func executeOperation() {
        operation()
    }
}

actor MyActor {
    var number: Int = 42
    
    func f() {
        let executor = Executor {
            self.number += 1
        }
        
        self.number += 1 // 1
        DispatchQueue.global().async {
            executor.executeOperation() // 2
        }
    }
}
```

このようなコードに対してもコンパイラが warning を出してくれます。

```swift
actor MyActor {
    func f() {
        // ...

        self.number += 1
        DispatchQueue.global().async {
            // ❗️ Capture of 'executor' with non-sendable type 'Executor' in a `@Sendable` closure
            executor.executeOperation()
        }
    }
}
```

ここでは `Executor` が `Sendable` に準拠していないことが warning の原因になっています。先ほども述べたように非同期処理を実行する API は `@Sendable` なクロージャを受け取るようになっており、 `Sendable` に準拠していない型である `Executor` のインスタンスはそのようなクロージャでキャプチャできないようになっているためです。

もちろんこの warning は `Executor` を `Sendable` に準拠させれば消えますが、そうするとまた別の warning が発生します。

```swift
struct Executor: Sendable {
    // ❗️ Stored property 'operation' of 'Sendable'-conforming struct 'Executor' has non-sendable type '() -> Void'
    private var operation: () -> Void
    
    // ...
}
```

`@Sendable` でないクロージャをメンバーに持つ型は `Sendable` に準拠することができないことがこの warning の原因です。このことから、クロージャが `@Sendable` でない限りはプロパティに保存されたとしても、クロージャが後で別のスレッドで実行されて actor とデータ競合を起こすということはコンパイラが防いでくれることがわかります。

ここまでの議論で、 `@Sendable` なクロージャは最終的に Concurrency ドメインの壁を越えられないのでそのクロージャが定義された actor のコンテキストを受け継いでも安全であることが納得できると思います。

## `SwiftUI.Button` の `action` は `@MainActor` なのか

それでは、記事の最初に提示した疑問である `SwiftUI.Button` の `action` は `@MainActor` なのかについて考えていきます。結論から書くと、`SwiftUI.Button` の `action` は `@MainActor` ではないと思います。

Xcode で `Button` の `init` にコードジャンプしてみると `@MainActor` のアトリビュートがついていないことがわかります。

```swift
public struct Button<Label> : View where Label : View {
    public init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)
}
```

また、 `@MainActor` のクロージャを直接渡すと warning が出ることも `action` が `@MainActor` ではないことを示しています。

```swift
@MainActor
final class ViewModel: ObservableObject {
    func onTapped() {
        print("onTapped")
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        // ❗️ Converting function value of type '@MainActor () -> ()' to '() -> Void' loses global actor 'MainActor'
        Button(action: viewModel.onTapped) { Text("Tap Me") }
    }
}
```

ここで、`action` が `@MainActor` でないとすると、以下のコードが warning を出さないことが不思議に思えます。

```swift
struct ContentView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        // ✅ OK
        // MainActor である viewModel.onTapped を同期的に呼び出せている
        Button(action: { viewModel.onTapped() }) { Text("Tap Me") }
    }
}
```

実際に、例えば上記の `Button` のケースと同じようなことをしているはずの以下のコードを書いてみると warning が出ます。

```swift
@MainActor func mainActor() {}

struct MyButton {
    init(action: () -> Void) {}
}

func f() {
    // ❗️ Call to main actor-isolated global function 'mainActor()' in a synchronous nonisolated context; this is an error in Swift 6
    let _ = MyButton(action: { mainActor() })
}
```

以上の疑問は、前の節で理解した「`@Sendable` でないクロージャは定義された actor のコンテキストを引き継ぐ」ということを考えると解消します。

まず、 SwiftUI のコードに Xcode でジャンプしてみると `SwiftUI.View` の `body` プロパティは `@MainActor` であることがわかります。

```swift
public protocol View {
    associatedtype Body : View

    @ViewBuilder @MainActor var body: Self.Body { get }
}
```

その上で、 `Button` の `action` は `@Sendable` ではないクロージャを受け取ります。

```swift
public struct Button<Label> : View where Label : View {
    public init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)
}
```

`@Sendable` でないクロージャは定義された actor のコンテキストを引き継ぐという前提のもと、

- `View` の `body` は `@MainActor` である
- `Button` の `action` に渡すクロージャは `@Sendable` ではない

という事実を考え合わせると、**`Button` の `action` に渡すクロージャをその場で定義した場合にそのクロージャが `@MainActor` のコンテキストを受け継ぐ**ことがわかります。これにより、中で `@MainActor` のメソッドを同期的に呼び出すことが可能になっているというわけです。

```swift
struct ContentView: View {
    // @MainActor
    @StateObject private var viewModel = ViewModel()

    // @MainActor
    var body: some View {
        // action に渡したクロージャは @MainActor を受け継ぐので 
        // @MainActor である onTapped メソッドを同期的に呼び出せる
        Button(action: { viewModel.onTapped() }) { Text("Tap Me") }
    }
}
```

以上の説を確認するため、 `action` に渡すクロージャをあえて `@Sendable` にしてみます。 `@Senadble` なクロージャは actor コンテキストを受け継がないのでこの変更によりクロージャが `@MainActor` ではなくなり、エラーが発生するようになります。

```diff_swift
struct ContentView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
-       Button(action: { viewModel.onTapped() }) { Text("Tap Me") }
        // ❌ Main actor-isolated property 'viewModel' can not be referenced from a Sendable closure
        // ❗️ Call to main actor-isolated instance method 'onTapped()' in a synchronous nonisolated context; this is an error in Swift 6
+       Button(action: { @Sendable in viewModel.onTapped() }) { Text("Tap Me") }
    }
}
```

別の観点からの確認として、 `@MainActor` ではない適当な関数で `Button` を定義して、 `action` のクロージャ内で `@MainActor` のメソッドを呼んでみると warning が発生します。これは `f` が `body` と異なり `@MainActor` ではないことから、 `f` の内部で定義したクロージャも `@MainActor` ではないためです。

```swift
@MainActor func mainActor() {}

func f() {
    // ❗️ Call to main actor-isolated global function 'mainActor()' in a synchronous nonisolated context; this is an error in Swift 6
    let _ = Button(action: { mainActor() }) { Text("Tap Me") }
}
```

この `f` 自体を `@MainActor` にしてあげると `body` と同じ状況になるため warning は消えます。

```swift
@MainActor
func f() {
    // ✅ OK
    let _ = Button(action: { mainActor() }) { Text("Tap Me") }
}
```

## 参考

- [SE-0306 Actors - Closures](https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md#closures)
- [Swift Concurrency チートシート - Case 20](https://zenn.dev/koher/articles/swift-concurrency-cheatsheet#%F0%9F%92%BC-case-20-(mainactor)%3A-%E5%85%B1%E6%9C%89%E3%81%95%E3%82%8C%E3%81%9F%E7%8A%B6%E6%85%8B%E3%81%AE%E5%A4%89%E6%9B%B4%EF%BC%88%E3%83%A1%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E4%B8%8A%E3%81%A7%E3%81%AE%E5%87%A6%E7%90%86%EF%BC%89)