Section 11/211 menit
11. Swift Concurrency: Task Cancellation
11. Swift Concurrency: Task Cancellation
Problem
Ketika user membuka layar lalu langsung menutupnya sebelum fetch selesai, request network tetap berjalan di background — membuang bandwidth dan CPU. Di UIKit dengan delegate/completion handler, kita harus menyimpan referensi ke request dan membatalkannya secara manual. Dengan Swift Concurrency, cancellation bersifat cooperative dan terstruktur.
Konsep
Taskadalah unit kerja yang bisa di-cancel viatask.cancel()- Cancellation bersifat cooperative: fungsi async harus secara aktif mengecek pembatalan dengan
try Task.checkCancellation()atau menunggu fungsi yang already-cancellable sepertiURLSession.data URLSession.data(for:)sudah mendukung cancellation otomatis — ketika Task di-cancel, ia melemparCancellationError- Penting: cancel tidak langsung menghentikan kode — ia hanya menandai Task sebagai "cancelled", dan kode harus kooperatif memeriksa tanda ini
Implementasi di Worker
swift
actor UserWorker {
// ...existing code...
func fetchUsers() async throws -> [User] {
// Periksa sebelum mulai — jika Task sudah di-cancel bahkan sebelum request,
// lempar CancellationError sekarang daripada memulai network request yang sia-sia.
try Task.checkCancellation()
let url = baseURL.appendingPathComponent("/users")
let request = URLRequest(url: url)
// URLSession.data sudah mendukung cancellation: jika Task di-cancel saat
// menunggu response, ini otomatis melempar CancellationError.
let (data, response) = try await session.data(for: request)
// Setelah operasi berat (network), cek lagi — siapa tahu Task di-cancel
// persis ketika response baru tiba. Tidak perlu memparse data yang tidak akan dipakai.
try Task.checkCancellation()
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw WorkerError.invalidResponse
}
let dtos = try JSONDecoder().decode([UserDTO].self, from: data)
return dtos.map { dto in
User(
id: String(dto.id),
name: dto.name,
email: dto.email,
avatarURL: URL(string: "https://i.pravatar.cc/150?u=\(dto.id)")
)
}
}
}
Implementasi di Interactor
swift
actor UserListInteractor: UserListBusinessLogic, UserListDataStore {
// ...existing code...
func fetchUsers(request: UserList.FetchUsers.Request) async {
guard !isLoading else { return }
isLoading = true
await presenter?.presentLoading(true)
do {
let users = try await worker.fetchUsers()
// Jika Task di-cancel selama worker.fetchUsers(), CancellationError
// akan di-catch di sini — kita bisa handle dengan bersih.
cachedUsers = users
await presenter?.presentUsers(UserList.FetchUsers.Response(users: users))
} catch is CancellationError {
// Cancellation bukan error yang perlu ditampilkan ke user.
// Cukup matikan loading — view sudah tidak relevan.
// Tidak perlu log atau alert.
} catch {
await presenter?.presentError(error)
}
isLoading = false
await presenter?.presentLoading(false)
}
}
Implementasi di ViewController
swift
@MainActor
final class UserListViewController: UIViewController {
var interactor: (any UserListBusinessLogic)?
var router: (any UserListRoutingLogic)?
// Simpan Task reference untuk bisa di-cancel sewaktu-waktu.
// 'Task<Void, Never>' karena kita handle error di dalam Task body.
private var fetchTask: Task<Void, Never>?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
fetchUsers()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Cancel fetch jika view sedang menghilang — misalnya user tap back.
// Task yang sudah cancel akan membersihkan dirinya sendiri.
fetchTask?.cancel()
}
private func fetchUsers() {
// Cancel request sebelumnya sebelum membuat yang baru.
// Ini penting untuk pull-to-refresh: mencegah dua fetch berjalan bersamaan
// dan response yang lebih lama menimpa response yang lebih baru.
fetchTask?.cancel()
fetchTask = Task {
await interactor?.fetchUsers(request: UserList.FetchUsers.Request())
}
}
private func handleRefresh() {
fetchTask?.cancel()
fetchTask = Task {
await interactor?.fetchUsers(request: UserList.FetchUsers.Request())
}
}
}
Menggunakan withTaskCancellationHandler untuk Cleanup
Ketika ada resource eksternal (timer, koneksi) yang harus dibersihkan saat cancel:
swift
actor UserWorker {
// Contoh: fetch dengan explicit cleanup saat cancel.
// withTaskCancellationHandler memungkinkan kita mendaftarkan callback
// yang dipanggil SEGERA saat Task di-cancel — bahkan jika Task sedang suspended.
func fetchUsersWithCleanup() async throws -> [User] {
var requestReference: URLSessionDataTask? = nil
return try await withTaskCancellationHandler {
// Body: kode yang dijalankan normal
try await withCheckedThrowingContinuation { continuation in
let task = session.dataTask(with: URLRequest(url: baseURL)) { data, _, error in
if let error {
continuation.resume(throwing: error)
} else if let data {
// parse...
continuation.resume(returning: []) // simplified
}
}
requestReference = task
task.resume()
}
} onCancel: {
// Handler ini dipanggil SEGERA saat Task.cancel() dipanggil,
// bahkan jika body sedang suspended menunggu network.
// Gunakan ini untuk membatalkan resource yang tidak bisa di-cancel secara async.
requestReference?.cancel()
}
}
}
Kapan Pakai withTaskCancellationHandler:
- Wrapping API lama yang punya cancel method sendiri (
URLSessionDataTask.cancel(),AVAudioPlayer.stop()) - Membersihkan subscription atau listener saat Task di-cancel
- Menutup koneksi database atau socket