.task { } — とは何か、async修飾子とViewでのデータ読み込み

著者: IT Sectr 公開日: 2026-06-26 読了時間: 9 分

.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表示時に非同期データ読み込みを行うSwiftUI修飾子、iOS 15以降で利用可能。
  • 自動キャンセル — View非表示時にSwiftUIがTaskをキャンセルし、メモリリークを防止。
  • async/awaitコンテキスト — .task内ではDispatchQueueやCombineなしでasync呼び出しが可能。
  • .task(id:) — 識別子付きバリアント。指定された値が変更されるとタスクを再起動。
  • Structured Concurrency — .taskは並列操作のためにStructured ConcurrencyとTaskGroupをサポート。

SwiftUIにおける.task { }とは

.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が自動的に行うため、ボイラープレートコードが削減され、タスクのキャンセルを忘れるリスクがなくなります。

swift
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
            }
        }
    }
}

.task vs .onAppear: 主な違い

多くの開発者は.onAppearでデータを読み込むことに慣れていますが、async/awaitと.taskの登場により、このアプローチは時代遅れになりました。.onAppearは同期的にコードを実行します — .onAppear内で非同期操作を行うには、呼び出しをTask { }でラップし、キャンセルのために手動でその参照を保持する必要があります。.taskはこれを自動的に行います。

特性.task { }.onAppear
Asyncコンテキストビルトインのasync/awaitTask { }ラッパーが必要
自動キャンセルはい、View非表示時いいえ、手動実装が必要
Structured Concurrencyサポート非サポート
再実行id変更時のみView表示のたびに
Apple推奨推奨される方法同期操作向け

.onAppear は同期操作にまだ有用です — 例えば、ログ記録や初期UI設定など。しかし、非同期データ読み込み、ネットワークリクエスト、データベースやファイルシステム操作には.taskを使用してください。アーキテクチャの観点からより安全でクリーンです。

swift
// ❌ レガシーアプローチ: .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:) — データ変更時の再起動

.task(id:) 修飾子は追加パラメータ — 識別子を受け入れます。識別子の値が変更されると、SwiftUIは現在のタスクをキャンセルし、新しい識別子で新しいタスクを開始します。これはデータが選択されたパラメータに依存する画面に最適です — 例えば、カテゴリ別の記事一覧やID別の製品詳細など。

swift
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を手動で管理する必要があります。

タスクのキャンセルとisCancelledチェック

.task はView非表示時にタスクを自動的にキャンセルしますが、非同期操作自体が協調的にキャンセルをチェックする必要があります。Swiftは協調的キャンセルモデルを使用します — Task.cancel()は実行を強制停止せず、isCancelledフラグを設定するだけです。タスク内のコードは定期的にこのフラグをチェックする必要があります。

swift
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の実践例

.task は多くのシナリオに適しています:単純なJSON読み込みからTaskGroupを使った複雑な並列操作まで。3つの典型的なユースケースを見てみましょう。

エラーハンドリング付き読み込み

swift
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のまま
            }
        }
    }
}

TaskGroupを使った並列読み込み

swift
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のよくある間違い

最も一般的な間違いは、メインスレッドに切り替えずに.task内でUIプロパティを変更することです。SwiftUIはasyncコンテキストで@Stateを変更する際に自動的に更新をメインスレッドに戻しますが、.task内でのUIKit要素の直接操作はクラッシュを引き起こす可能性があります。

  • try/catch忘れ — .taskは自動的にエラーを処理しません。スローする関数はすべてdo/catchでラップする必要があります。そうしないとアプリがクラッシュします。
  • 競合状態 — 複数の.task(id:)が異なるidで開始され、同じ状態を更新する場合、競合状態が発生する可能性があります。異なるデータソースには別々のプロパティを使用してください。
  • 長時間の同期操作 — .taskは同期コードを非同期にしません。.task内に重い同期処理がある場合は、Task.detachedでラップするか、別のasyncメソッドに移動してください。
  • CancellationErrorの無視 — Task.checkCancellation()をチェックする際、CancellationErrorは上位に伝播させるべきで、抑制すべきではありません。キャンセルの抑制はメモリリークにつながる可能性があります。
swift
// ❌ エラー: エラーハンドリングなし
.task {
    let data = await fetchData() // エラー発生時にクラッシュ!
    items = data
}

// ✅ 正しい: do/catch
.task {
    do {
        items = await fetchData()
    } catch {
        errorMessage = error.localizedDescription
    }
}

よくある質問

.taskと.onAppearでTask { }を使う違いは何ですか?

.taskはView非表示時にタスクを自動キャンセルし、Structured Concurrencyをサポートします。.onAppearでのTask { }はタスクへの参照を手動で保持し、.onDisappearでcancel()を呼び出す必要があります。.taskは読みやすく、データ読み込みがViewのライフサイクルの一部であることを明示的に示します。

.taskをpublisherの購読に使用できますか?

はい、AsyncSequenceやAsyncStreamを購読するには、.task内でfor await value in publisher.valuesを使用します。これはPublisher.values拡張を介してasync/awaitとCombineの両方で動作します。Viewが非表示になると、反復は自動的に終了し、購読はキャンセルされます。

.taskはTabViewとどう動作しますか — タブ切り替え時にタスクは再起動しますか?

はい、TabView内のViewが切り替えのたびに再作成される場合です。iOS 18以降、TabViewはViewをメモリに保持できるため、その場合.taskは再起動しません。タブ切り替えのたびにデータを再読み込みする必要がある場合は、タブ識別子付きの.task(id:)を使用してください。

.taskを持つViewがリクエスト完了前に非表示になるとどうなりますか?

SwiftUIはView非表示時にタスクをキャンセルします。URLSessionリクエストがタスク内にあった場合、それもキャンセルされます。タスクが協調的キャンセルをサポートしていない場合(isCancelledをチェックしないなど)、実行は継続されますが、Viewがもう存在しないため結果は状態に適用されません。

1つのViewに複数の.task修飾子を使用できますか?

はい、1つのViewに複数の.task修飾子を追加できます。それぞれが独立したタスクを作成します。これは異なるデータソースを分離するのに便利です:プロフィール読み込み用の.task、WebSocket購読用の.task、位置情報監視用の.taskなど。

まとめ

  • .task { } — View非表示時に自動キャンセル付きの非同期操作のためのSwiftUI修飾子。
  • async/awaitサポート — .task内ではTaskラッパーなしで完全なasyncコンテキストが利用可能。
  • .task(id:) — 識別子変更時にタスクを再起動し、手動.onChangeを置き換え。
  • 協調的キャンセル — タスク内でキャンセルをチェックするにはTask.checkCancellation()を使用。
  • Structured Concurrency — .taskは子タスクのキャンセル付きのTaskGroupと並列操作をサポート。
  • .onAppearの代替 — 非同期データ読み込みには.onAppear + Task + .onDisappearパターンの代わりに.taskを使用。
  • エラーハンドリング — クラッシュを防ぐため、.task内のすべてのasync呼び出しはdo/catchでラップする必要があります。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください