SwiftUIのtask modifierのボディ部分はメインスレッドで実行される
SwiftUIには .task
というmodifierがあります。
task
は onAppear
と同様にビューが表示される前に処理が一度だけ実行されます。
大きな違いは 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) です。
- ビューの表示時に
task
モディファイアが呼び出されます - まずはasyncで
asyncHeavyWork
メソッドを実行します- この実行が終わるまではボタンをクリックすることができます
- 5秒後に
asyncHeavyWork
が終了し、次に同期でheavyWork
メソッドを実行します- この実行はメインスレッドで行われるためボタンはクリックできません
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処理は別スレッドで実行される
-
OSLogStore#getEntries (アプリ自身のログを取得する) を使っていたところ数秒かかる処理だったため、無事GUIが固まりました。 ↩︎
Discussion