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の使い方 | View で Task {} に処理を詰め込みすぎる | 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 + Combine | DispatchQueue / async/await / Combine が混在 | Concurrencyに寄せる or 役割を分けて使う | 切り替え時は中途半端にせず整理して一貫性を保つ |
まとめ
今回の5部構成でGPT先生にSwift Concurrencyをざっくり学んだ感想。
“概念は理解したけどこれは書かないと本当には理解できないな、、、”
自分は凡人タイプなので10回言われてもわからないことがザラにある、コーディングもまさに。
なのでConcurrencyも記事を書きながらわかったつもりでまだまだわかってないことが大半だと思うので、次はConcurrencyをふんだんに使った簡単なアプリを作ってみようと思います。
(おまけ)
GPT先生にSwift Concurrencyの理解に苦しむ人々に励ましの画像をってPromptを投げた結果。
