.task { } はiOS 15で導入されたSwiftUIの修飾子で、Viewが表示されると非同期操作を開始し、Viewが非表示になると自動的にキャンセルします。キャンセル機能なしで同期コードを実行する.onAppearとは異なり、.taskはasync/awaitコンテキストで動作し、Viewのライフサイクルを考慮します。Viewが非表示になると、SwiftUIは作成されたTaskに対してcancel()を呼び出します。これにより、メモリリークや、Viewの更新が不要になった後の操作の実行を防ぎます。Apple WWDC Session 10132 — Meet async/await in SwiftUI (2024) によると、.taskはStructured Concurrencyと安全に連携し、非同期操作のライフタイムを自動的に管理するため、SwiftUIでデータを読み込む推奨方法です。
重要なポイント
.task { } は、Viewが画面に表示されるとasyncコンテキストでTaskを作成するView修飾子です。SwiftUIは提供されたクロージャをバックグラウンドスレッドで実行し、メインスレッドをUI操作用に解放したままにします。Viewが非表示になると、SwiftUIはStructured Concurrencyメカニズムを通じてTaskを自動的にキャンセルします。これにより、非同期操作の結果が不要になった後も実行が継続されないことが保証されます。
Apple — Swift Programming Language (2025) によると、.taskはSwift 5.5で導入されたStructured Concurrencyの概念を使用します。各.taskは親Viewのタスクの下に子タスクを作成します。親タスクがキャンセルされると(Viewが非表示になると)、すべての子タスクも自動的にキャンセルされます。これにより、DispatchWorkItemやAnyCancellableへの参照を手動で保存する場合と比較して、非同期操作のライフサイクル管理が根本的に簡素化されます。
@State + .onAppearでの手動呼び出しという従来のアプローチとは異なり、.taskは後でキャンセルするためにTaskへの参照を保存する必要がありません。SwiftUIが自動的に行うため、ボイラープレートコードが削減され、タスクのキャンセルを忘れるリスクがなくなります。
struct ArticlesView: View {
@State var articles: [Article] = []
@State var error: Error?
var body: some View {
List(articles) { article in
Text(article.title)
}
.task {
do {
articles = await APIClient().fetchArticles()
} catch {
self.error = error
}
}
}
}
多くの開発者は.onAppearでデータを読み込むことに慣れていますが、async/awaitと.taskの登場により、このアプローチは時代遅れになりました。.onAppearは同期的にコードを実行します — .onAppear内で非同期操作を行うには、呼び出しをTask { }でラップし、キャンセルのために手動でその参照を保持する必要があります。.taskはこれを自動的に行います。
| 特性 | .task { } | .onAppear |
|---|---|---|
| Asyncコンテキスト | ビルトインのasync/await | Task { }ラッパーが必要 |
| 自動キャンセル | はい、View非表示時 | いいえ、手動実装が必要 |
| Structured Concurrency | サポート | 非サポート |
| 再実行 | id変更時のみ | View表示のたびに |
| Apple推奨 | 推奨される方法 | 同期操作向け |
.onAppear は同期操作にまだ有用です — 例えば、ログ記録や初期UI設定など。しかし、非同期データ読み込み、ネットワークリクエスト、データベースやファイルシステム操作には.taskを使用してください。アーキテクチャの観点からより安全でクリーンです。
// ❌ レガシーアプローチ: .onAppearでのキャンセルなしのTask
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
.onAppear {
loadTask = Task { await loadData() }
}
.onDisappear { loadTask?.cancel() }
// ✅ モダンなアプローチ: .taskがキャンセルを管理
func body() { var body: some View { Text("") }
.task { await loadData() }
.task(id:) 修飾子は追加パラメータ — 識別子を受け入れます。識別子の値が変更されると、SwiftUIは現在のタスクをキャンセルし、新しい識別子で新しいタスクを開始します。これはデータが選択されたパラメータに依存する画面に最適です — 例えば、カテゴリ別の記事一覧やID別の製品詳細など。
struct CategoryView: View {
let categoryId: Int
@State var items: [Item] = []
var body: some View {
List(items) { item in
Text(item.name)
}
.task(id: categoryId) {
await loadItems(for: categoryId)
}
}
func loadItems(for id: Int) async {
do {
items = await APIClient().fetchItems(categoryId: id)
} catch {
// エラーを処理
}
}
}
categoryId が変更されると、SwiftUIは以前のリクエストをキャンセルして新しいものを開始します。これは特に素早いカテゴリ切り替え時に重要です — 古いリクエストが新しいリクエストと状態更新を競合しません。.task(id:)がない場合、.onChangeを通じて手動で変更を追跡し、Taskを手動で管理する必要があります。
.task はView非表示時にタスクを自動的にキャンセルしますが、非同期操作自体が協調的にキャンセルをチェックする必要があります。Swiftは協調的キャンセルモデルを使用します — Task.cancel()は実行を強制停止せず、isCancelledフラグを設定するだけです。タスク内のコードは定期的にこのフラグをチェックする必要があります。
struct LoadingView: View {
@State var progress: Double = 0
var body: some View {
ProgressView(value: progress)
.task {
for i in 0..<100 {
// キャンセルを確認
try Task.checkCancellation()
await Task.sleep(nanoseconds: 50_000_000)
progress = Double(i + 1) / 100.0
}
}
}
}
Task.checkCancellation() はタスクがキャンセルされた場合にCancellationErrorをスローします。これが最もシンプルなチェック方法です — 任意のasyncコンテキストで動作します。別の方法として、コストのかかる操作の前にTask.isCancelledを手動でチェックすることもできます。URLSessionの場合、タスクがキャンセルされるとネットワークリクエストも自動的にキャンセルされます。URLSessionはStructured Concurrencyをネイティブサポートしているためです。
.task は多くのシナリオに適しています:単純なJSON読み込みからTaskGroupを使った複雑な並列操作まで。3つの典型的なユースケースを見てみましょう。
struct ProfileView: View {
@State var profile: Profile?
@State var isLoading = true
var body: some View {
Group {
if isLoading {
ProgressView()
} else if let profile {
Text(profile.name)
} else {
Text("読み込みに失敗")
}
}
.task {
defer { isLoading = false }
do {
profile = await APIClient().fetchProfile()
} catch {
// プロフィールがnilのまま
}
}
}
}
struct DashboardView: View {
@State var stats: DashboardStats?
var body: some View {
Text("ダッシュボード")
.task {
stats = await Task {
await withThrowingTaskGroup { group in
group.addTask { await API().fetchUsers() }
group.addTask { await API().fetchOrders() }
group.addTask { await API().fetchRevenue() }
return DashboardStats(
users: try await group.next(),
orders: try await group.next(),
revenue: try await group.next()
)
}
}.value
}
}
}
最も一般的な間違いは、メインスレッドに切り替えずに.task内でUIプロパティを変更することです。SwiftUIはasyncコンテキストで@Stateを変更する際に自動的に更新をメインスレッドに戻しますが、.task内でのUIKit要素の直接操作はクラッシュを引き起こす可能性があります。
// ❌ エラー: エラーハンドリングなし
.task {
let data = await fetchData() // エラー発生時にクラッシュ!
items = data
}
// ✅ 正しい: do/catch
.task {
do {
items = await fetchData()
} catch {
errorMessage = error.localizedDescription
}
}
よくある質問
.taskはView非表示時にタスクを自動キャンセルし、Structured Concurrencyをサポートします。.onAppearでのTask { }はタスクへの参照を手動で保持し、.onDisappearでcancel()を呼び出す必要があります。.taskは読みやすく、データ読み込みがViewのライフサイクルの一部であることを明示的に示します。
はい、AsyncSequenceやAsyncStreamを購読するには、.task内でfor await value in publisher.valuesを使用します。これはPublisher.values拡張を介してasync/awaitとCombineの両方で動作します。Viewが非表示になると、反復は自動的に終了し、購読はキャンセルされます。
はい、TabView内のViewが切り替えのたびに再作成される場合です。iOS 18以降、TabViewはViewをメモリに保持できるため、その場合.taskは再起動しません。タブ切り替えのたびにデータを再読み込みする必要がある場合は、タブ識別子付きの.task(id:)を使用してください。
SwiftUIはView非表示時にタスクをキャンセルします。URLSessionリクエストがタスク内にあった場合、それもキャンセルされます。タスクが協調的キャンセルをサポートしていない場合(isCancelledをチェックしないなど)、実行は継続されますが、Viewがもう存在しないため結果は状態に適用されません。
はい、1つのViewに複数の.task修飾子を追加できます。それぞれが独立したタスクを作成します。これは異なるデータソースを分離するのに便利です:プロフィール読み込み用の.task、WebSocket購読用の.task、位置情報監視用の.taskなど。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。