Section 13/211 menit
13. Swift Concurrency: TaskGroup — Bulk Avatar Prefetch
13. Swift Concurrency: TaskGroup — Bulk Avatar Prefetch
Problem
Setelah user list tampil, kita ingin pre-load semua avatar agar cell langsung menampilkan gambar tanpa delay. Jumlah user tidak diketahui saat compile-time — bisa 5, bisa 100. async let tidak bisa menangani jumlah dinamis.
Konsep
withTaskGroup / withThrowingTaskGroup menjalankan sejumlah task secara concurrent dengan jumlah yang ditentukan saat runtime. Task ditambahkan dengan group.addTask {} dan hasilnya dikumpulkan dengan for await. Ada juga concurrency limiting untuk mencegah terlalu banyak request sekaligus.
Implementasi AvatarCache
swift
// Cache untuk menyimpan data avatar agar tidak di-download ulang.
// Dijadikan actor untuk thread-safe read/write dari banyak concurrent task.
actor AvatarCache {
static let shared = AvatarCache()
private init() {}
private var cache: [String: Data] = [:]
func data(for userID: String) -> Data? {
cache[userID]
}
func store(_ data: Data, for userID: String) {
cache[userID] = data
}
func clear() {
cache.removeAll()
}
}
Implementasi TaskGroup di Worker
swift
actor UserWorker {
// ...existing methods...
// Prefetch avatar untuk semua user secara concurrent tapi terbatas.
// 'maxConcurrent' mencegah kita membuka terlalu banyak koneksi sekaligus
// yang bisa menyebabkan throttling dari server atau exhausting thread pool.
func prefetchAvatars(
for users: [User],
maxConcurrent: Int = 5
) async -> [String: Data] {
// Kita pakai withTaskGroup (bukan throwing) karena avatar adalah
// operasi best-effort — jika satu gagal, lanjutkan yang lain.
await withTaskGroup(of: (String, Data?).self) { group in
var results: [String: Data] = [:]
// Chunking manual untuk membatasi concurrency.
// Tanpa ini, jika ada 100 user, kita membuka 100 koneksi sekaligus.
let chunks = users.chunked(into: maxConcurrent)
for chunk in chunks {
// Mulai fetch untuk satu chunk — max 'maxConcurrent' task bersamaan.
for user in chunk {
guard let url = user.avatarURL else { continue }
group.addTask {
// Cek cache dulu sebelum download.
if let cached = await AvatarCache.shared.data(for: user.id) {
return (user.id, cached)
}
let data = try? await URLSession.shared.data(from: url).0
return (user.id, data)
}
}
// Kumpulkan hasil chunk ini sebelum memulai chunk berikutnya.
// Ini adalah cara manual membatasi concurrency — tunggu satu batch
// selesai sebelum mulai batch berikutnya.
for await (id, data) in group {
if let data {
results[id] = data
// Simpan ke cache untuk request berikutnya.
await AvatarCache.shared.store(data, for: id)
}
}
}
return results
}
}
}
// Helper extension untuk chunking array
private extension Array {
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}
Alternatif: TaskGroup dengan Partial Results
Scenario: tampilkan user segera, lalu update cell satu per satu ketika avatar-nya siap.
swift
actor UserListInteractor: UserListBusinessLogic, UserListDataStore {
private let avatarCache = AvatarCache.shared
// Trigger prefetch di background — tidak blocking, tidak await.
// Interactor mendelegasikan ke worker dan hasilnya di-update ke presenter
// satu per satu ketika tiap avatar selesai diload.
func prefetchAvatars(for users: [User]) {
// Task.detached karena kita tidak ingin propagate cancellation dari caller.
// Prefetch adalah operasi opsional — jika view hilang, tetap simpan ke cache
// agar berguna jika user buka layar ini lagi.
Task.detached(priority: .utility) { [weak self] in
guard let self else { return }
await withTaskGroup(of: (String, Data?).self) { group in
for user in users {
guard let url = user.avatarURL else { continue }
group.addTask {
let data = try? await URLSession.shared.data(from: url).0
return (user.id, data)
}
}
// for await di sini memberikan partial results —
// setiap avatar yang selesai langsung dikirim ke presenter.
for await (id, data) in group {
guard let data else { continue }
await AvatarCache.shared.store(data, for: id)
// Notify presenter bahwa avatar untuk user ini sudah siap.
// Presenter bisa update cell individual tanpa reload seluruh table.
await self.presenter?.presentAvatarReady(userID: id, data: data)
}
}
}
}
}
Kapan Pakai TaskGroup:
- Jumlah task dinamis (tidak diketahui compile-time)
- Ingin partial results (tampilkan hasil begitu tersedia, tidak perlu semua selesai)
- Operasi bulk yang perlu dibatasi concurrency-nya
Kapan Tidak Pakai TaskGroup:
- Jumlah task kecil dan tetap (2-5) →
async letlebih ringkas - Semua task harus sukses agar hasilnya berguna →
async letdengan throwing semantics lebih jelas