Para que un cálculo de Swift se pueda cancelar, conserva el identificador de su tarea, llama a cancel() cuando se solicite y comprueba si se ha cancelado dentro del bucle. Este ejemplo para macOS también mantiene la interfaz receptiva y evita que una ejecución cancelada muestre un resultado tardío.
¿Qué hace realmente cancelar una tarea de Swift?
Swift usa la cancelación cooperativa: solicitar la cancelación marca una tarea, pero el código en ejecución decide cuándo detenerse. Un bucle de CPU que nunca comprueba si se ha cancelado puede continuar hasta terminar. Al llamar a Task.checkCancellation() se genera CancellationError cuando se detecta la cancelación. Documentación de Apple sobre cancelación, checkCancellation().
El objetivo de este ejemplo es concreto: añadir botones funcionales Start y Cancel a una pequeña app para Mac que calcula una suma sintética. Cada entrada se genera localmente. El cálculo suma las raíces cuadradas de enteros positivos consecutivos y produce un único Double solo cuando se han procesado todos los términos solicitados.
Elige el contrato del resultado antes de implementar la cancelación. Este ejemplo descarta una suma incompleta e informa la cancelación por separado. Devolver una suma parcial requeriría otro contrato que indique cuántos términos se incluyeron; de lo contrario, quien llama podría confundir un cálculo incompleto con el resultado solicitado. La guía del lenguaje Swift documenta como posibles respuestas a la cancelación lanzar un error, devolver un resultado vacío o devolver el trabajo parcial. Concurrencia.
Hay dos responsabilidades que implementar: detener los cálculos innecesarios y decidir si un valor calculado sigue teniendo cabida en la interfaz. El ejemplo se ocupa de ambas, incluso si la cancelación se solicita después de la última comprobación del proceso de trabajo, pero antes de que se muestre el resultado.
Añade comprobaciones de cancelación dentro del cálculo
Crea en Xcode un proyecto de app para macOS con SwiftUI y Swift. Para este ejemplo, usa el modo de lenguaje Swift 6 y un destino de implementación macOS 13 o posterior. Añade el siguiente archivo completo, llamado SyntheticCalculation.swift. No depende de la vista.
// SyntheticCalculation.swift
import Swift
enum SyntheticCalculation {
nonisolated static func sumOfSquareRoots(
terms: Int,
checkEvery: Int = 4_096
) throws -> Double {
precondition(terms >= 0, "terms must be nonnegative")
precondition(checkEvery > 0, "checkEvery must be positive")
try Task.checkCancellation()
var completed = 0
var total = 0.0
while completed < terms {
try Task.checkCancellation()
let count = min(checkEvery, terms - completed)
let end = completed + count
while completed < end {
completed += 1
total += Double(completed).squareRoot()
}
}
try Task.checkCancellation()
return total
}
}
La función comprueba si se ha cancelado antes de empezar, en el límite de cada bloque y antes de devolver el resultado. Estos son puntos de detención deliberados: si se solicita la cancelación durante un bloque, se detectará en la siguiente comprobación. Task.checkCancellation() es síncrono, así que el cálculo puede seguir siendo una función ordinaria que lanza errores; no necesita un await solo para comprobar si se ha cancelado. checkCancellation().
El cálculo por bloques también mantiene claros los límites de entrada. Cero términos produce cero si la tarea no se ha cancelado. Los términos negativos y un tamaño de bloque no positivo son errores de programación que se hacen cumplir mediante precondiciones. Calcular cada bloque a partir de la cantidad restante evita avanzar su extremo más allá de terms.
El acumulador permanece en el proceso de trabajo. Si se lanza un error de cancelación, la función termina sin devolverlo, lo que implementa el contrato elegido de devolver solo resultados completos. Como se trata de una suma de punto flotante, el valor final es una aproximación de la suma matemática.
Mantén receptivo el botón Cancel
Las comprobaciones de cancelación no sirven si la acción del botón no tiene oportunidad de ejecutarse. Apple explica que colocar trabajo síncrono pesado dentro de un Task común creado desde una vista de SwiftUI puede seguir ocupando el actor principal, porque esa tarea hereda el contexto de actor que la rodea. Apple documenta una tarea separada como método admitido para ejecutar trabajo síncrono fuera de ese contexto heredado. Mejorar la capacidad de respuesta de la app.
Aquí, el proceso de trabajo separado captura solo un entero inmutable y llama a una función síncrona marcada explícitamente con nonisolated. Todo el estado de la interfaz permanece en el actor principal. Una tarea independiente en el actor principal espera el valor del proceso de trabajo y actualiza la pantalla.
Reemplaza el archivo de entrada generado para la app por el siguiente archivo completo y elimina ContentView.swift si sigue presente. Mantén exactamente una declaración @main en el destino, como exige el ciclo de vida de las apps de SwiftUI. Estos dos archivos conforman la app de ejemplo. App.
// CancellationDemoApp.swift
import Foundation
import SwiftUI
@main
struct CancellationDemoApp: App {
var body: some Scene {
WindowGroup {
CalculationView()
}
}
}
@MainActor
struct CalculationView: View {
@State private var worker: Task<Double, Error>?
@State private var runID: UUID?
@State private var status = "Ready"
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Synthetic square-root sum")
.font(.headline)
Text(status)
.textSelection(.enabled)
HStack {
Button("Start") {
start()
}
.disabled(worker != nil)
Button("Cancel") {
guard let worker else { return }
worker.cancel()
status = "Cancellation requested"
}
.disabled(worker == nil)
}
}
.padding(24)
.frame(minWidth: 420, minHeight: 160)
.onDisappear {
worker?.cancel()
runID = nil
worker = nil
status = "Ready"
}
}
private func start() {
guard worker == nil else { return }
let id = UUID()
let terms = 500_000_000
runID = id
status = "Calculating"
let run = Task.detached(priority: .userInitiated) {
try SyntheticCalculation.sumOfSquareRoots(terms: terms)
}
worker = run
Task { @MainActor in
do {
let value = try await run.value
guard runID == id else { return }
if run.isCancelled {
status = "Cancelled"
} else {
status = "Result: \(value)"
}
} catch is CancellationError {
guard runID == id else { return }
status = "Cancelled"
} catch {
guard runID == id else { return }
status = "Failed: \(error)"
}
guard runID == id else { return }
worker = nil
runID = nil
}
}
}
Es esencial conservar worker. Descartar el identificador de una tarea no la cancela; te impide cancelarla mediante ese identificador. Esperar value del proceso de trabajo que puede lanzar errores también permite detectar el error. Las tareas separadas requieren una gestión explícita de su ciclo de vida, así que el botón y el controlador de desaparición cancelan directamente el proceso de trabajo. Cancelar solo la tarea que espera su resultado no propagaría la cancelación a este proceso de trabajo separado. Documentación sobre tareas separadas.
UUID de Foundation proporciona un identificador para cada ejecución. La comparación de identificadores es una regla de la aplicación: una vez que la desaparición invalida una ejecución, su finalización posterior no puede actualizar el estado actual de esta vista. El identificador no detiene los cálculos; de eso se encargan worker.cancel() y las comprobaciones del bucle. UUID.
El botón Start permanece desactivado hasta que el proceso de trabajo termina o lanza un error. Al pulsar Cancel, primero aparece Cancellation requested; el observador de finalización lo cambia a Cancelled cuando gestiona el resultado. Así, la solicitud de cancelación se distingue de la confirmación de que el proceso de trabajo ha terminado.
¿Cómo se debe gestionar un resultado tardío?
Considera esta secuencia: el proceso de trabajo pasa la última comprobación, termina y, después, el usuario pulsa Cancel antes de que el actor principal acepte su valor. Apple señala explícitamente que la cancelación después de la última comprobación posible puede no tener ningún efecto observable en el proceso de trabajo. Una comprobación final reduce ese intervalo, pero no puede eliminarlo. cancel().
Por eso, el ejemplo comprueba run.isCancelled después de esperar un valor correcto. Esta es una comprobación del identificador del proceso de trabajo. Usar ahí la propiedad estática Task.isCancelled comprobaría la tarea observadora. La guía del lenguaje Swift distingue entre comprobar la tarea actual mediante la propiedad del tipo y comprobar otra tarea mediante la propiedad de su instancia. Concurrencia.
Esto establece una regla de aceptación concreta: si la cancelación se procesa antes de aceptar el resultado, prevalece la cancelación. Una vez que la aceptación se ha completado y se ha eliminado el proceso de trabajo, el botón Cancel, desactivado, no tiene ningún resultado terminado que retirar. La regla sigue el orden de las acciones del actor principal y no promete que la cancelación revierta un trabajo ya completado.
Elige un intervalo útil entre comprobaciones de cancelación
El tamaño de bloque predeterminado de 4,096 términos es un punto de partida ilustrativo. Los bloques más pequeños ofrecen oportunidades más frecuentes para detectar la cancelación y ejecutan más comprobaciones. Los bloques más grandes reducen la frecuencia de las comprobaciones y permiten hacer más cálculos entre los puntos de detención.
Para hacer una estimación aproximada, supongamos que un bloque contiene 4,096 términos y que cada término tarda dos microsegundos a modo de ejemplo. Ese bloque representa unos 8.2 milisegundos de cálculo. Son valores inventados para demostrar la operación, no mediciones de Swift ni de un Mac en particular. La planificación y las actualizaciones de la interfaz pueden añadir más demora.
| Observación | Qué inspeccionar o cambiar |
|---|---|
| La interfaz deja de responder durante el cálculo | Comprueba que el bucle se ejecute en el proceso de trabajo separado y permanezca fuera del aislamiento del actor principal. |
| La interfaz responde, pero la cancelación tarda demasiado | Reduce checkEvery y mide la duración del bloque más largo. |
| Más adelante se añade una operación costosa a un bloque | Añade puntos de detención dentro de esa operación si su implementación lo permite. |
| Una ejecución cancelada muestra un resultado | Comprueba el identificador del proceso de trabajo al aceptar el resultado y conserva la protección del identificador de ejecución. |
No añadas un controlador de cancelación solo para establecer otra marca para este bucle. La tarea ya expone su estado de cancelación y la guía del lenguaje Swift explica que instalar un controlador sigue dejando la detención a cargo de la cooperación del código. Para este cálculo local, las comprobaciones explícitas ofrecen el mecanismo de detención necesario. Concurrencia.
Verifica la finalización, la cancelación y las ejecuciones repetidas
En el entorno de investigación no había un compilador de Swift ni un entorno de ejecución de macOS disponibles, así que este ejemplo se inspeccionó, pero no se compiló ni se ejecutó. Compila ambos archivos en Xcode y verifica estos comportamientos por separado:
- Resultado pequeño completado: cambia temporalmente
termsa4. El resultado mostrado debería ser aproximadamente6.146264369941973, con posibles diferencias por redondeo de punto flotante. - Entrada vacía: cambia
termsa0. Una ejecución sin cancelar debería mostrar cero. - Cancelación durante el cálculo: restaura
500_000_000, pulsaStarty luegoCancelmientras se está ejecutando. Espera verCancellation requestedseguido deCancelled, sin ningún resultado parcial. - Otra ejecución: después de que se complete la cancelación, vuelve a pulsar
Start. La nueva ejecución debería comenzar con normalidad. - Desaparición de la vista: cierra una ventana mientras se realiza un cálculo. El controlador de desaparición debería solicitar la cancelación e invalidar las actualizaciones de la interfaz de esa ejecución.
El gran número de términos busca dejar tiempo para interactuar, pero su duración depende del Mac y de la configuración de compilación. Si termina antes de que puedas cancelarlo, aumenta la cantidad sin superar el rango de enteros admitido. Valida la aritmética de una ejecución completada con la entrada pequeña, en lugar de esperar a que termine la demostración grande.
El diseño terminado tiene una ruta clara para detener el cálculo y una regla clara para aceptar el resultado: el botón cancela el proceso de trabajo conservado, los límites de los bloques permiten que el cálculo lance un error y el actor principal solo acepta un resultado no cancelado de la ejecución actual. Esas tres decisiones hacen que la cancelación sea útil en un cálculo corriente para Mac.
Fuentes
- Concurrencia — El lenguaje de programación Swift, proyecto Swift.
- checkCancellation() — Documentación para desarrolladores de Apple.
- cancel() — Documentación para desarrolladores de Apple.
- Mejorar la capacidad de respuesta de la app — Documentación para desarrolladores de Apple.
- detached(name:priority:operation:) — Documentación para desarrolladores de Apple.
- App — Documentación para desarrolladores de Apple.
- UUID — Documentación para desarrolladores de Apple.