⛓️

SwiftUIのtask modifierのボディ部分はメインスレッドで実行される

2024/01/05に公開

SwiftUIには .task というmodifierがあります。
https://developer.apple.com/documentation/swiftui/view/task(priority:_:)

taskonAppear と同様にビューが表示される前に処理が一度だけ実行されます。
大きな違いは onAppear とは違い、task はasyncな処理を実行できることです。
これは onAppear の処理部分 (第一引数) が (() -> Void)? なのに対して task の処理部分 (第二引数) は @escaping () async -> Void になっていることからもわかります。

ところで、task モディファイアのボディ部分自体はメインスレッドで実行されます。
そのため task の処理中で @State@Binding 変数に書き込むことができるわけですが、うっかり重い処理をawaitをつけずに実行してしまうとUIが固まります。

筆者はmacOSアプリでasync対応されてない重いAPI呼び出し [1]task のボディ部分で実行したところレインボーカーソルが出て気付くことができました。

簡単なコードで試してみます。
動作確認環境は Xcode 15.1 + iPhone 15 (iOS 17.2) です。

  1. ビューの表示時に task モディファイアが呼び出されます
  2. まずはasyncで asyncHeavyWorkメソッドを実行します
    1. この実行が終わるまではボタンをクリックすることができます
  3. 5秒後に asyncHeavyWork が終了し、次に同期で heavyWork メソッドを実行します
    1. この実行はメインスレッドで行われるためボタンはクリックできません
import SwiftUI

struct ContentView: View {
    @State var message = "Loading…"
    var body: some View {
        VStack {
            Text(message)
            Button(action: {
                message = "Clicked"
            }, label: {
                Text("Click")
            })
        }
        .padding()
        .task {
            message = "Start asyncHeavyWork"
            // taskモディファイアのボディはメインスレッドで処理されるが、
            // awaitをつけて呼び出したasync処理は別スレッドで実行される
            message = await asyncHeavyWork()

            message = "Start heavyWork"
            // awaitをつけずに実行すれば必ずメインスレッドで実行される
            // そのためheavyWork実行中はボタンをタップできない
            message = heavyWork()
        }
    }

    private func asyncHeavyWork() async -> String {
        // taskから呼び出されたasyncメソッドは別スレッドで実行される
        sleep(5)
        return "Done asyncHeavyWork"
    }

    private func heavyWork() -> String {
        // taskと同じスレッドで実行される
        sleep(5)
        return "Done heavyWork"
    }
}

筆者はちゃんとわかってなかったのですが、taskモディファイアのボディから実行するasync処理は (あえてmain actorをisolateしたasync処理でもなければ) 必ずメインスレッドとは別のスレッドで実行されるそうです。

async 関数はあえて main actor に isolate されていない限り必ずバックグラウンドスレッドで実行されます。これは、 actor に isolate されていない async 関数は、 default concurrent executor によってバックグラウンドで実行されるためです。
https://zenn.dev/mayaa/articles/792297c2a47935#async-関数の呼び出し

筆者の環境ではXcode上でasyncメソッド内でブレークポイントをしかけてスレッドを見たところ別スレッドになっているようなのを確認してこれが仕様なのかどうかよくわからずにおっかなびっくりasyncを使っていました。Swift Concurrencyムズい…。

taskモディファイアでasync処理を実行したあとに @State 変数を書き換えたりするメインスレッドで処理されることを期待してよいかも気になるかもしれないのですが、おそらくtaskモディフィアは上の記事でいうところの「main actor に isolate された処理」なので、awaitしたあともメインスレッドであることが保証されるということなのだろうと思います。

まとめ

  • task モディファイアは表示時に一度だけ実行されasync処理を呼び出すことができる
  • task モディファイアの処理ボディはメインスレッドで実行されるので重い処理はGUIをブロックしたりするので気をつけよう
  • task モディファイアから呼び出されたasync処理は別スレッドで実行される
脚注
  1. OSLogStore#getEntries (アプリ自身のログを取得する) を使っていたところ数秒かかる処理だったため、無事GUIが固まりました。 ↩︎

Discussion