Section 12/211 menit
12. Swift Concurrency: async let — Parallel Fetch
12. Swift Concurrency: async let — Parallel Fetch
Problem
Banyak layar butuh data dari beberapa sumber sekaligus. Cara naif adalah fetch satu per satu secara sequential — lambat dan tidak efisien padahal tidak ada dependensi antar request.
Konsep
async let menjalankan beberapa operasi async secara bersamaan tanpa harus menunggu satu selesai sebelum memulai yang lain. Hasil baru diambil saat semua sudah selesai dengan await. Ini adalah structured concurrency — semua child task otomatis di-cancel jika salah satu throw.
Scenario: Fetch Users + Stats Bersamaan
Tambahkan method baru ke Worker:
swift
actor UserWorker {
// ...existing methods...
// Fetch statistik user (endpoint berbeda dari /users)
func fetchUserStats() async throws -> UserStats {
let url = baseURL.appendingPathComponent("/posts/count") // contoh endpoint
let (data, _) = try await session.data(from: url)
return try JSONDecoder().decode(UserStats.self, from: data)
}
// Fetch satu featured user (misalnya dari endpoint /users/featured)
func fetchFeaturedUser() async throws -> User? {
let url = baseURL.appendingPathComponent("/users/1") // simplified
let (data, _) = try await session.data(from: url)
let dto = try JSONDecoder().decode(UserDTO.self, from: data)
return User(id: String(dto.id), name: dto.name, email: dto.email, avatarURL: nil)
}
}
struct UserStats: Sendable, Decodable {
let totalUsers: Int
let activeUsers: Int
}
Update Response model untuk menyertakan stats:
swift
enum UserList {
enum FetchUsers {
struct Request: Sendable {}
struct Response: Sendable {
let users: [User]
let stats: UserStats? // tambahan
let featuredUser: User? // tambahan
}
struct ViewModel: Sendable {
struct DisplayedUser: Sendable {
let id: String
let fullName: String
let emailLabel: String
let avatarURL: URL?
}
let displayedUsers: [DisplayedUser]
let statsLabel: String? // "10 users aktif dari 50 total"
let featuredUser: DisplayedUser?
}
}
}
Implementasi async let di Interactor:
swift
actor UserListInteractor: UserListBusinessLogic, UserListDataStore {
func fetchUsers(request: UserList.FetchUsers.Request) async {
guard !isLoading else { return }
isLoading = true
await presenter?.presentLoading(true)
do {
// TANPA async let (sequential — lambat):
// let users = try await worker.fetchUsers() // tunggu selesai
// let stats = try await worker.fetchUserStats() // baru mulai ini
// let featured = try await worker.fetchFeaturedUser() // baru mulai ini
// Total waktu = A + B + C
// DENGAN async let (parallel — cepat):
// Ketiga request dimulai BERSAMAAN.
// 'async let' adalah deklarasi: "mulai hitung ini sekarang,
// aku akan ambil hasilnya nanti saat butuh."
async let users = worker.fetchUsers()
async let stats = worker.fetchUserStats()
async let featured = worker.fetchFeaturedUser()
// 'await' baru terjadi di sini — menunggu SEMUA selesai.
// Jika salah satu throw, yang lain otomatis di-cancel (structured concurrency).
// Total waktu ≈ max(A, B, C) — biasanya jauh lebih cepat.
let (fetchedUsers, fetchedStats, featuredUser) = try await (users, stats, featured)
cachedUsers = fetchedUsers
let response = UserList.FetchUsers.Response(
users: fetchedUsers,
stats: fetchedStats,
featuredUser: featuredUser
)
await presenter?.presentUsers(response)
} catch {
await presenter?.presentError(error)
}
isLoading = false
await presenter?.presentLoading(false)
}
}
Update Presenter untuk format stats:
swift
@MainActor
final class UserListPresenter: UserListPresentationLogic {
func presentUsers(_ response: UserList.FetchUsers.Response) {
let displayedUsers = response.users.map { user in
UserList.FetchUsers.ViewModel.DisplayedUser(
id: user.id,
fullName: user.name.split(separator: " ").map { $0.capitalized }.joined(separator: " "),
emailLabel: user.email.lowercased(),
avatarURL: user.avatarURL
)
}
// Format stats — logic ini ada di Presenter, bukan di View.
let statsLabel: String? = response.stats.map { stats in
"\(stats.activeUsers) user aktif dari \(stats.totalUsers) total"
}
let featuredUser = response.featuredUser.map { user in
UserList.FetchUsers.ViewModel.DisplayedUser(
id: user.id,
fullName: user.name,
emailLabel: user.email,
avatarURL: user.avatarURL
)
}
let viewModel = UserList.FetchUsers.ViewModel(
displayedUsers: displayedUsers,
statsLabel: statsLabel,
featuredUser: featuredUser
)
viewController?.displayUsers(viewModel)
}
// ...other methods...
}
Kapan Pakai async let:
- Fetch 2–5 sumber data bersamaan yang hasilnya semua dibutuhkan
- Jumlah task sudah diketahui saat compile-time
- Jika salah satu gagal, tidak perlu melanjutkan yang lain
Kapan Tidak Pakai async let:
- Jumlah task dinamis (tidak diketahui saat compile-time) → gunakan
TaskGroup - Ingin partial result (tampilkan data yang sudah selesai tanpa menunggu semua) → gunakan
TaskGroupdenganfor await