.task { } es un modificador de SwiftUI introducido en iOS 15 que inicia una operación asíncrona cuando aparece una View y la cancela automáticamente cuando desaparece. A diferencia de .onAppear, que ejecuta código síncrono sin posibilidad de cancelación, .task funciona con el contexto async/await y respeta el ciclo de vida de la View: cuando la View desaparece, SwiftUI llama a cancel() en el Task creado. Esto evita fugas de memoria y la ejecución de operaciones después de que la View ya no necesita actualizarse. Según Apple WWDC Session 10132 — Meet async/await in SwiftUI (2024), .task es la forma preferida de cargar datos en SwiftUI porque trabaja de forma segura con Structured Concurrency y gestiona automáticamente el tiempo de vida de la operación asíncrona.
Puntos Clave
.task { } es un modificador de View que crea un Task en un contexto async cuando la View aparece en pantalla. SwiftUI ejecuta el cierre proporcionado en un hilo secundario, dejando el hilo principal libre para operaciones de UI. Cuando la View desaparece, SwiftUI cancela automáticamente el Task mediante el mecanismo de Structured Concurrency — esto garantiza que la operación asíncrona no continúe ejecutándose después de que su resultado ya no sea necesario.
Según Apple — Swift Programming Language (2025), .task utiliza el concepto de Structured Concurrency introducido en Swift 5.5. Cada .task crea una tarea hija dentro de la tarea de la View padre. Si la tarea padre se cancela (la View desaparece), todas las tareas hijas también se cancelan automáticamente. Esto simplifica radicalmente la gestión del ciclo de vida de las operaciones asíncronas en comparación con almacenar manualmente referencias a DispatchWorkItem o AnyCancellable.
A diferencia del enfoque tradicional con @State + llamadas manuales en .onAppear, .task no requiere almacenar una referencia al Task para su posterior cancelación. SwiftUI lo hace automáticamente, reduciendo el código repetitivo y eliminando el riesgo de olvidar cancelar una tarea.
struct ArticlesView: View {
@State var articles: [Article] = []
@State var error: Error?
var body: some View {
List(articles) { article in
Text(article.title)
}
.task {
do {
articles = await APIClient().fetchArticles()
} catch {
self.error = error
}
}
}
}
Muchos desarrolladores están acostumbrados a cargar datos en .onAppear, pero con la llegada de async/await y .task, este enfoque ha quedado obsoleto. .onAppear ejecuta código de forma síncrona — para operaciones async dentro de .onAppear, es necesario envolver la llamada en Task { } y mantener manualmente una referencia para posible cancelación. .task hace esto automáticamente.
| Característica | .task { } | .onAppear |
|---|---|---|
| Contexto async | Async/await integrado | Requiere envoltorio Task { } |
| Auto-cancelación | Sí, al desaparecer la View | No, debe implementarse manualmente |
| Structured Concurrency | Soporta | No soporta |
| Re-ejecución | Solo cuando cambia id | Cada vez que aparece la View |
| Recomendación Apple | Enfoque preferido | Para operaciones síncronas |
.onAppear sigue siendo útil para operaciones síncronas — por ejemplo, registro o configuración inicial de UI. Pero para carga asíncrona de datos, solicitudes de red, operaciones con base de datos o sistema de archivos, usa .task. Es más seguro y limpio desde el punto de vista arquitectónico.
// ❌ Enfoque antiguo: Task en .onAppear sin cancelación
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
.onAppear {
loadTask = Task { await loadData() }
}
.onDisappear { loadTask?.cancel() }
// ✅ Enfoque moderno: .task gestiona la cancelación
func body() { var body: some View { Text("") }
.task { await loadData() }
El modificador .task(id:) acepta un parámetro adicional — un identificador. Cuando el valor del identificador cambia, SwiftUI cancela la tarea actual e inicia una nueva con el nuevo identificador. Esto es ideal para pantallas donde los datos dependen de un parámetro seleccionado — por ejemplo, una lista de artículos por categoría o detalles de un producto por ID.
struct CategoryView: View {
let categoryId: Int
@State var items: [Item] = []
var body: some View {
List(items) { item in
Text(item.name)
}
.task(id: categoryId) {
await loadItems(for: categoryId)
}
}
func loadItems(for id: Int) async {
do {
items = await APIClient().fetchItems(categoryId: id)
} catch {
// manejar error
}
}
}
Cuando categoryId cambia, SwiftUI cancela la solicitud anterior e inicia una nueva. Esto es especialmente importante para cambios rápidos de categoría — las solicitudes antiguas no competirán con las nuevas por la actualización del estado. Sin .task(id:), tendrías que rastrear manualmente los cambios mediante .onChange y gestionar el Task manualmente.
Aunque .task cancela automáticamente la tarea cuando la View desaparece, la operación asíncrona en sí misma debe verificar cooperativamente la cancelación. Swift usa un modelo de cancelación cooperativa — Task.cancel() no detiene la ejecución forzosamente, sino que solo establece el flag isCancelled. El código dentro de la tarea debe verificar periódicamente este flag.
struct LoadingView: View {
@State var progress: Double = 0
var body: some View {
ProgressView(value: progress)
.task {
for i in 0..<100 {
// Verificar cancelación
try Task.checkCancellation()
await Task.sleep(nanoseconds: 50_000_000)
progress = Double(i + 1) / 100.0
}
}
}
}
Task.checkCancellation() lanza CancellationError si la tarea fue cancelada. Esta es la forma más simple de verificación — funciona en cualquier contexto async. Una alternativa es verificar Task.isCancelled manualmente antes de operaciones costosas. Para URLSession, las solicitudes de red se cancelan automáticamente cuando se cancela la tarea, ya que URLSession soporta Structured Concurrency de forma nativa.
.task es adecuado para muchos escenarios: desde carga simple de JSON hasta operaciones paralelas complejas con TaskGroup. Veamos tres casos de uso típicos.
struct ProfileView: View {
@State var profile: Profile?
@State var isLoading = true
var body: some View {
Group {
if isLoading {
ProgressView()
} else if let profile {
Text(profile.name)
} else {
Text("Error al cargar")
}
}
.task {
defer { isLoading = false }
do {
profile = await APIClient().fetchProfile()
} catch {
// perfil sigue siendo nil
}
}
}
}
struct DashboardView: View {
@State var stats: DashboardStats?
var body: some View {
Text("Panel")
.task {
stats = await Task {
await withThrowingTaskGroup { group in
group.addTask { await API().fetchUsers() }
group.addTask { await API().fetchOrders() }
group.addTask { await API().fetchRevenue() }
return DashboardStats(
users: try await group.next(),
orders: try await group.next(),
revenue: try await group.next()
)
}
}.value
}
}
}
El error más común es mutar propiedades de UI dentro de .task sin cambiar al hilo principal. Aunque SwiftUI devuelve automáticamente las actualizaciones al hilo principal al modificar @State en un contexto async, las manipulaciones directas de elementos UIKit dentro de .task pueden causar un crash.
// ❌ Error: sin manejo de errores
.task {
let data = await fetchData() // falla al producirse un error!
items = data
}
// ✅ Correcto: do/catch
.task {
do {
items = await fetchData()
} catch {
errorMessage = error.localizedDescription
}
}
Preguntas Frecuentes
.task cancela automáticamente la tarea cuando la View desaparece y soporta Structured Concurrency. Task { } en .onAppear requiere mantener manualmente una referencia a la tarea y llamar a cancel() en .onDisappear. .task también es más fácil de leer — indica explícitamente que la carga de datos es parte del ciclo de vida de la View.
Sí, para suscribirse a un AsyncSequence o AsyncStream, usa for await value in publisher.values dentro de .task. Esto funciona tanto con async/await como con Combine a través de la extensión Publisher.values. Cuando la View desaparece, la iteración terminará automáticamente y la suscripción se cancelará.
Sí, si la View dentro de TabView se recrea en cada cambio. A partir de iOS 18, TabView puede mantener las Views en memoria — en ese caso .task no se reinicia. Usa .task(id:) con un identificador de pestaña si necesitas recargar datos en cada cambio de pestaña.
SwiftUI cancelará la tarea cuando la View desaparezca. Si la solicitud URLSession estaba dentro de la tarea, también se cancelará. Si la tarea no soporta cancelación cooperativa (por ejemplo, no verifica isCancelled), continuará ejecutándose, pero su resultado no se aplicará al estado porque la View ya no existe.
Sí, puedes agregar varios modificadores .task a una sola View. Cada uno crea una tarea independiente. Esto es útil para separar diferentes fuentes de datos: un .task para cargar un perfil, otro para suscribirse a un WebSocket, un tercero para monitorear la geolocalización.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también