[Xcode/Swift] GPT先生からSwift Concurrencyを学んだ⑤(設計 × Concurrency)

Swift Concurrency、書けるようにはなった。
でもふと思う——このコード、設計的にアリ?実務で通用する?

async let も TaskGroup も、なんとなく使ってるけど…
アーキテクチャにどう組み込むのか」がいちばん難しい?

そんな悩みを一つずつ解消して、
非同期 × 設計で“実務で戦えるコード”に進化させる、それがこの記事の目的。

ViewModel での async let / TaskGroup の使いどころ

Q: ViewModelで並列処理はアリ or ナシ 🐜🍐

A: アリ、でも使い方次第

@MainActor
final class HomeViewModel: ObservableObject {
    @Published var user: User?
    @Published var posts: [Post] = []
    @Published var error: String?

    func fetchAll() {
        Task {
            do {
                async let user = repository.fetchUser()
                async let posts = repository.fetchPosts()
                // 同時に開始、awaitでまとめて取得
                self.user = try await user
                self.posts = try await posts
            } catch {
                self.error = "取得失敗: \(error.localizedDescription)"
            }
        }
    }
}

使い所:

  • 小規模アプリ (わざわざUseCase層・Interactor層に分けるまでもないくらいのVMボリューム)
  • 画面表示に複数の非同期リソースが必要
  • 各処理に依存関係がない(並列でOK)
  • キャンセル対応も視野に入れたいなら TaskGroup の方が良い
アプリの規模・性質ViewModelに書いてOK?UseCase分離が欲しい?
小規模・MVP✅ YES❌ NO(やりすぎ)
非同期処理1〜2個✅ YES❌ NO
並列取得(async let)✅ YES❌ NO
複数データ取得(TaskGroup)✅ YES(ロジック小なら)⚠️ 条件次第
ドメインロジックが絡む❌ やや複雑化✅ YES
処理の再利用が必要❌ 冗長になる✅ YES

Repository層での async 化(Firebaseとか)

昔は completionHandler 地獄、今は async/await でスッキリ書ける。

protocol UserRepository {
    func fetchUser() async throws -> User
}

final class FirebaseUserRepository: UserRepository {
    func fetchUser() async throws -> User {
        let snapshot = try await Firestore.firestore()
            .collection("users")
            .document("uid123")
            .getDocument()
        
        return try snapshot.data(as: User.self)
    }
}
  • Firebaseも getDocument() など async に対応してる(Swift SDK)
  • リポジトリでは「非同期の抽象化」を意識する
    「使う側(ViewModel)」は非同期かどうか意識しなくて済むように

キャンセル可能な UI 処理の設計例

実務では「ユーザーがタップを連打したら?」に強くなるコードが求められる。
キャンセル対応してるViewModelはUIのストレスを劇的に下げられる。

@MainActor
final class SearchViewModel: ObservableObject {
    @Published var result: [Item] = []

    private var searchTask: Task<Void, Never>?

    func search(query: String) {
        searchTask?.cancel() // 以前の検索処理を中断

        searchTask = Task {
            do {
                try await Task.sleep(nanoseconds: 300_000_000) // debounce風
                let result = try await repository.searchItems(query: query)
                self.result = result
            } catch {
                // キャンセルなら無視
            }
        }
    }
}

Concurrency の注意点 & アンチパターン

カテゴリ❌ アンチパターン例✅ 正しい使い方解説
@MainActor背景スレッドで @Published 書き換え DispatchQueue.main.async 多用ViewModelやメソッドに @MainActor 付与UI更新は MainActor 経由で。古い手法との混在はバグの温床
Taskの使い方ViewTask {} に処理を詰め込みすぎるViewModel側に処理をまとめて task {} で呼び出しViewがロジック知るのは責務過多。規模が増すと破綻する
非同期キャンセルTask.cancel() だけで止まると思ってるcheckCancellation()Task 内部で必ず呼ぶキャンセルされても処理が走るのは、チェック不足が原因
UIタップ系処理検索やタップ連打時に Task が溜まる前の Task を cancel() して再生成複数Taskが走ると競合発生。UXガタ落ち
Viewの責務Viewでデータ取得やawait連発ViewModelに処理を委譲し、@Publishedで受け取るViewは「表示」に集中。データ取得は裏側に任せるべき
Taskの並列性単に await を順番に並べるだけasync let / TaskGroup を活用し並列処理にする非同期“風”コードじゃ並列にならず、パフォーマンスも落ちる
Concurrency + CombineDispatchQueue / async/await / Combine が混在Concurrencyに寄せる or 役割を分けて使う切り替え時は中途半端にせず整理して一貫性を保つ

まとめ

今回の5部構成でGPT先生にSwift Concurrencyをざっくり学んだ感想。

“概念は理解したけどこれは書かないと本当には理解できないな、、、”

自分は凡人タイプなので10回言われてもわからないことがザラにある、コーディングもまさに。

なのでConcurrencyも記事を書きながらわかったつもりでまだまだわかってないことが大半だと思うので、次はConcurrencyをふんだんに使った簡単なアプリを作ってみようと思います。

(おまけ)

GPT先生にSwift Concurrencyの理解に苦しむ人々に励ましの画像をってPromptを投げた結果。