Audita el Swift generado por IA en busca de Task.detached
¿Ves Task.detached en Swift generado por IA? Detente y revísalo. Una tarea detached sin una estrategia de cancelación, sin una prioridad explícita o sin un dueño claro es una señal de alerta documentada en Swift Concurrency — y los asistentes de código recurren a ella mucho más seguido de lo que el lenguaje recomienda. La mayoría de las veces lo que querías era un Task común.
Qué descarta realmente Task.detached
Un Task { ... } común no estructurado hereda tres cosas del contexto que lo lanzó. Task.detached { ... } no hereda ninguna:
| Propiedad | Task común | Task.detached |
|---|---|---|
| Prioridad de la tarea | Heredada de la tarea actual | Corre por defecto; hay que fijarla explícitamente |
| Valores task-local | Heredados | Se pierden |
| Aislamiento de actor | Heredado del contexto envolvente | Ninguno — corre sin aislamiento |
La cancelación sigue funcionando en una tarea detached si conservas el handle y llamas a .cancel() — pero una tarea detached no forma parte del árbol de tareas que la rodea, así que no se cancela automáticamente cuando se cancela su padre. Esa es la trampa: dispara un Task.detached desde una vista que desaparece y el trabajo sigue corriendo sin nadie que lo detenga.
Por qué el asistente recurre a ella
Las tareas detached son la forma tosca de escapar de un actor. Cuando un asistente está dentro de un tipo @MainActor y quiere hacer trabajo en segundo plano, Task.detached compila y "simplemente corre fuera del hilo principal" — por eso parece correcto. Pero normalmente no quieres cortar el aislamiento por completo; quieres que esa llamada pesada específica corra en otro lado y que el resultado vuelva al main actor para actualizar la UI. Un Task común más await sobre un método de un actor hace exactamente eso, mantiene la cancelación conectada al árbol y mantiene las mutaciones de UI en @MainActor.
La lista de auditoría
Cuando veas Task.detached en código generado, pregunta:
- ¿La prioridad está fijada a propósito? Si no, corre con la prioridad por defecto, no la del llamador — a menudo una ralentización sutil.
- ¿Quién la cancela? Si nada conserva el handle, el trabajo puede sobrevivir a su razón de existir. La concurrencia estructurada (
async let, task groups) o unTaskcomún atado a un ciclo de vida suele ser mejor. - ¿Toca estado mutable compartido o la UI? Si es así, debería estar aislada en un actor — deja que un actor sea dueño del estado y mantén el trabajo de UI en
@MainActor, en vez de hacer detach y volver a entrar.
Si las tres respuestas son "sin razón", reescríbela como un Task común.
Jugada avanzada: deja que el actor haga el aislamiento
// Salida del asistente — corta el aislamiento sin motivo
Task.detached {
let data = try await loadHeavyThing()
await MainActor.run { self.items = data } // salto manual de vuelta
}
// Auditado — el Task común hereda el contexto; el actor es dueño del trabajo
Task {
let data = try await repository.loadHeavyThing() // repository es un actor
self.items = data // ya está en @MainActor
}
El Task común hereda el aislamiento @MainActor del tipo envolvente, así que la asignación self.items es segura sin el baile de MainActor.run. El trabajo pesado queda aislado dentro del actor repository, fuera del hilo principal, y la cancelación se propaga con el ciclo de vida envolvente. Reserva Task.detached para el caso raro en que de verdad necesitas correr independiente del contexto actual — y cuando lo hagas, fija la prioridad y conserva el handle.
Recursos
- What's the difference between a task and a detached task? — Hacking with Swift
- Understanding unstructured and detached tasks in Swift — Donny Wals
- Problematic Swift Concurrency Patterns — Matt Massicotte
- Task — Swift Standard Library (Apple Developer)
¿Envías Swift con un asistente de IA? Yeda AI audita el código generado por IA en busca de los patrones que pasan la revisión pero fallan en producción.