[Xcode/Swift] GPT先生からSwift Concurrencyを学んだ③ (TaskGroup)

TaskGroupとは何か

TaskGroup(または withThrowingTaskGroup)= 動的に子タスクを組める並列処理のコンテナ、子タスクをどんどん追加して全部完了するまで待つ仕組み。

画像を大量にダウンロードする時。URLリストが動的でも、forループで addTask して一緒に並列処理できるのが強み。

async let との違い

比較軸async letTaskGroup
タスク数書く前に数が確定している必要がある動的に必要なだけ子タスクを追加できる
構文簡潔で使いやすいやや複雑だけど柔軟性が高い
エラー/キャンセル片方が失敗したら全体キャンセル(即)個別に制御やキャンセル可能
結果取得まとめて await するfor await … in group で逐次取得可能

どんな場面で使うか:

  • 処理数が動的 → APIの応答数に応じて処理数が変わる時
  • キャンセル条件がある → 一定数成功したら他を止めたい時など
  • 大量並列処理 → 100 件以上とか大量データを非同期で処理したい時

サンプルコード

import SwiftUI
import UIKit

struct HomeView: View {
    @State private var images: [UIImage] = []
    @State private var isLoading = false
    @State private var errorMessage: String?

    // 画像のURLリスト, ランダムなIDを使用して、ランダムな画像を生成
    private let imageURLs: [URL] = (1...10).compactMap { _ in
        let randomID = Int.random(in: 1...1_000)
        return URL(string: "https://picsum.photos/id/\(randomID)/160/160")
    }

    var body: some View {
        NavigationView {
            VStack(spacing: 16) {
                if isLoading {
                    ProgressView("Loading images...")
                } else if let errorMessage = errorMessage {
                    Text(errorMessage)
                        .foregroundColor(.red)
                        .padding()
                } else {
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack {
                            ForEach(Array(images.enumerated()), id: \.offset) { index, image in
                                Image(uiImage: image)
                                    .resizable()
                                    .frame(width: 160, height: 160)
                                    .cornerRadius(12)
                            }
                        }
                        .padding(.horizontal)
                    }
                }
                Button("Download Images") {
                    Task {
                        await downloadAllImages()
                    }
                }
                .padding()
            }
            .navigationTitle("TaskGroup Demo")
        }
    }

    // TaskGroupで画像を非同期で取得
    private func downloadAllImages() async {
        isLoading = true
        errorMessage = nil
        images = []

        do {
            let downloadedImages = try await withThrowingTaskGroup(of: UIImage?.self) { group in
                // 各URLから画像を非同期でダウンロード
                for url in imageURLs {
                    // TaskGroupにタスクを追加
                    group.addTask {
                        let (data, _) = try await URLSession.shared.data(from: url)
                        return UIImage(data: data)
                    }
                }
                var result: [UIImage] = []

                // タスクの結果を集める
                for try await image in group {
                    if let img = image {
                        result.append(img)
                    }
                }
                return result
            }

            self.images = downloadedImages
            isLoading = false
        } catch {
            // エラー処理
            self.errorMessage = "画像の取得に失敗しました: \(error.localizedDescription)"
            isLoading = false
        }
    }
}

取得結果の順序を保ちたい場合

↑のコードでは、処理の完了順にappendされるので順番は考慮されない、順番も考慮したいなら、index付きでaddTaskを行う。

try await withThrowingTaskGroup(of: (Int, UIImage?).self) { group in
    for (index, url) in imageURLs.enumerated() {
        group.addTask {
            let (data, _) = try await URLSession.shared.data(from: url)
            let image = UIImage(data: data)
            return (index, image) // indexとセットで返す
        }
    }

    var resultArray = Array<UIImage?>(repeating: nil, count: imageURLs.count)

    for try await (index, image) in group {
        resultArray[index] = image
    }

    self.images = resultArray.compactMap { $0 }
}
  • enumerated()indexとURLのセットにする
  • group.addTask でタプル (index, image) を返す
  • resultArray を先に必要数で初期化して、index指定で代入
  • 最後に compactMap で nil を除去して完成

条件付きで途中キャンセルしたい場合

実務でありがちなこと:

  • 10件中、3件成功したらOK。他はキャンセルしたい
  • 特定条件(例:最初にエラーが出たら)で全部止めたい

キャンセル処理付き TaskGroup 例:

try await withThrowingTaskGroup(of: UIImage?.self) { group in
    for url in imageURLs {
        group.addTask {
            if Task.isCancelled { return nil }
            let (data, _) = try await URLSession.shared.data(from: url)
            return UIImage(data: data)
        }
    }

    var downloaded: [UIImage] = []

    for try await image in group {
        if let img = image {
            downloaded.append(img)
        }

        if downloaded.count >= 5 {
            group.cancelAll() // 一定数ダウンロードしたら残りをキャンセル
        }
    }

    self.images = downloaded
}

まとめ

  • TaskGroup は、動的に非同期タスクを並列実行できる超便利な仕組み
  • async let より柔軟で、大量処理や順序制御に向いてる
  • addTask で処理をどんどん追加して、for await で結果を回収
  • withThrowingTaskGroup1件でも throw したら全体キャンセル
パターン使いどころ
通常の TaskGroup複数処理を一気に並列したいとき
順番を保つ TaskGroupUIで順序が重要なとき(例:画像表示)
条件付きキャンセル TaskGroup「3件成功したら十分」「最初の失敗で中断」みたいな制御が必要なとき

いきなり全てを覚えようとするとパンクするので、分割して理解する、これ大事。

↓ 次回

[Xcode/Swift] GPT先生からSwift Concurrencyを学んだ④ (非同期トラブル、actorなど)