SC #8: Cancelando un Task
Source: Dev.to
Nota: En Swift, cancelar una
Taskno garantiza que la ejecución se detenga inmediatamente. CadaTaskdebe comprobar manualmente si ha sido cancelada (por ejemplo, usandoTask.checkCancellation()oTask.isCancelled).
1. Ejemplo básico: descargar una imagen
func fetchImage() async throws -> UIImage? {
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://httpbin.org/image")!
var imageRequest = URLRequest(url: imageURL)
imageRequest.allHTTPHeaderFields = ["accept": "image/jpeg"]
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(for: imageRequest)
return UIImage(data: imageData)
}
// La tarea se cancela inmediatamente después de ser creada.
imageTask.cancel()
// Esperamos el valor (o el error) de la tarea.
return try await imageTask.value
}
Qué ocurre
| Paso | Descripción |
|---|---|
| 1️⃣ | Se envuelve la llamada a URLSession.shared.data(for:) dentro de un Task que devuelve UIImage?. |
| 2️⃣ | Se cancela la tarea con imageTask.cancel(). |
| 3️⃣ | Se espera el resultado con try await imageTask.value. |
| 4️⃣ | La petición de red se inicia antes de que la cancelación tenga efecto, pero URLSession respeta la señal de cancelación y aborta la solicitud. |
Salida de consola
Starting network request...
Image loading failed: Error Domain=NSURLErrorDomain Code=-999 "cancelled"
2. Task.checkCancellation()
Task.checkCancellation() lanza automáticamente un CancellationError si la tarea ha sido cancelada.
func fetchImage() async throws -> UIImage? {
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://httpbin.org/image")!
// ...
try Task.checkCancellation() // La petición web no se lleva a cabo porque la tarea ya estaba cancelada cuando se llamó a `checkCancellation()`.
}
}
3. Task.isCancelled
Task.isCancelled permite inspeccionar el estado de cancelación sin lanzar una excepción; podemos actuar manualmente.
func fetchImage() async throws -> UIImage? {
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://httpbin.org/image")!
// ...
guard Task.isCancelled == false else {
print("Image request was cancelled")
return nil
}
let (imageData, _) = try await URLSession.shared.data(for: imageRequest)
// ...
}
imageTask.cancel()
return try await imageTask.value
}
Salida
Image request was cancelled
4. ¿Es necesario envolver una función async en un Task?
Solo es necesario crear explícitamente un Task cuando queremos cancelar la operación de forma deliberada. Si no necesitamos cancelación manual, podemos escribir la función sin el wrapper:
func fetchImage() async throws -> UIImage? {
let imageURL = URL(string: "https://httpbin.org/image")!
var imageRequest = URLRequest(url: imageURL)
imageRequest.allHTTPHeaderFields = ["accept": "image/jpeg"]
try Task.checkCancellation()
let (imageData, _) = try await URLSession.shared.data(for: imageRequest)
try Task.checkCancellation()
return UIImage(data: imageData)
}
5. Cancelación manual en SwiftUI
5.1 Guardar la referencia de la tarea
@State private var image: UIImage?
@State private var imageDownloadingTask: Task?
var body: some View {
VStack {
if let image {
Image(uiImage: image)
} else {
Text("Loading...")
}
}
.onAppear {
imageDownloadingTask = Task {
do {
image = try await fetchImage()
print("Image loading completed")
} catch {
print("Image loading failed: \(error)")
}
}
}
.onDisappear {
imageDownloadingTask?.cancel()
}
}
5.2 Usar el modificador .task(priority:_: )
SwiftUI crea y cancela automáticamente la tarea cuando la vista desaparece.
@State private var image: UIImage?
var body: some View {
VStack {
if let image {
Image(uiImage: image)
} else {
Text("Loading...")
}
}
.task {
do {
image = try await fetchImage()
print("Image loading completed")
} catch {
print("Image loading failed: \(error)")
}
}
}
Importante: El código dentro de
.taskse ejecuta antes que cualquier código lanzado desde.onAppear. Sin embargo, el momento exacto de ejecución de la tarea no está garantizado; un código síncrono en.onAppearpodría ejecutarse antes que la tarea de.task.
6. Cancelación en jerarquías de tareas (supertarea → subtareas)
Cuando una supertarea se cancela, todas sus subtareas reciben la señal de cancelación. Cada subtarea debe comprobar explícitamente su estado (isCancelled o checkCancellation()). Si no lo hace, seguirá ejecutándose.
Ejemplo
func parentTask() async {
let handler = Task {
print("Parent task started")
async let child1 = childTask(id: 1)
async let child2 = childTask(id: 2)
// Esperamos a que ambas subtareas terminen.
_ = try await [child1, child2]
}
// Simulamos algún trabajo y luego cancelamos.
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 s
handler.cancel()
do {
_ = try await handler.value
} catch let error as CancellationError {
print("La tarea fue cancelada")
} catch {
print("Hubo otro error")
}
print("Parent task finished")
}
func childTask(id: Int) async throws -> Int {
for i in 1..<4 {
try Task.checkCancellation() // <‑‑ verifica cancelación
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 s
print("Subtarea: \(id), paso: \(i)")
}
print("Terminé la subtarea \(id)")
return id
}
Posibles salidas
- Si la supertarea se cancela antes de que las subtareas terminen, cada iteración de
childTasklanzaráCancellationErrory la ejecución se detendrá. - Si alguna subtarea no verifica la cancelación, seguirá corriendo aunque la supertarea haya sido cancelada.
7. Resumen rápido
| Herramienta | Qué hace | ¿Lanza excepción? |
|---|---|---|
task.cancel() | Señala que la tarea debe detenerse. | No |
Task.checkCancellation() | Lanza CancellationError si la tarea está cancelada. | Sí |
Task.isCancelled | Devuelve true si la tarea está cancelada. | No (se usa en guard/if). |
.task {} (SwiftUI) | Crea una tarea que se cancela automáticamente al desaparecer la vista. | No (pero la tarea interna puede lanzar). |
Con estos conceptos podrás controlar y optimizar la cancelación de operaciones asíncronas en tus aplicaciones Swift y SwiftUI. ¡A programar!
btarea: \(id)")
return id
}