.task { } è un modificatore in SwiftUI introdotto in iOS 15 che avvia un'operazione asincrona quando una View appare e la cancella automaticamente quando la View scompare. A differenza di .onAppear, che esegue codice sincrono senza possibilità di cancellazione, .task funziona con il contesto async/await e rispetta il ciclo di vita della View: quando la View scompare, SwiftUI chiama cancel() sul Task creato. Ciò previene perdite di memoria e l'esecuzione di operazioni dopo che la View non ha più bisogno di essere aggiornata. Secondo Apple WWDC Session 10132 — Meet async/await in SwiftUI (2024), .task è il modo preferito per caricare dati in SwiftUI perché funziona in sicurezza con Structured Concurrency e gestisce automaticamente la durata dell'operazione asincrona.
Punti Chiave
.task { } è un modificatore di View che crea un Task in un contesto async quando la View appare sullo schermo. SwiftUI esegue la chiusura fornita in un thread di background, lasciando il thread principale libero per le operazioni UI. Quando la View scompare, SwiftUI cancella automaticamente il Task attraverso il meccanismo Structured Concurrency — questo garantisce che l'operazione asincrona non continui ad eseguire dopo che il suo risultato non è più necessario.
Secondo Apple — Swift Programming Language (2025), .task utilizza il concetto di Structured Concurrency introdotto in Swift 5.5. Ogni .task crea un'attività figlia nell'ambito dell'attività della View padre. Se l'attività padre viene cancellata (la View scompare), tutte le attività figlie vengono anch'esse cancellate automaticamente. Questo semplifica radicalmente la gestione del ciclo di vita delle operazioni asincrone rispetto alla memorizzazione manuale di riferimenti a DispatchWorkItem o AnyCancellable.
A differenza dell'approccio tradizionale con @State + chiamata manuale in .onAppear, .task non richiede di memorizzare un riferimento al Task per una successiva cancellazione. SwiftUI lo fa automaticamente, riducendo il codice boilerplate ed eliminando il rischio di dimenticare di cancellare un'attività.
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
}
}
}
}
Molti sviluppatori sono abituati a caricare dati in .onAppear, ma con l'arrivo di async/await e .task, questo approccio è diventato obsoleto. .onAppear esegue il codice in modo sincrono — per operazioni asincrone all'interno di .onAppear, è necessario avvolgere la chiamata in Task { } e mantenere manualmente un riferimento per una possibile cancellazione. .task lo fa automaticamente.
| Caratteristica | .task { } | .onAppear |
|---|---|---|
| Contesto async | Async/await integrato | Richiede wrapper Task { } |
| Auto-cancellazione | Sì, quando la View scompare | No, deve essere implementata manualmente |
| Structured Concurrency | Supporta | Non supporta |
| Riesecuzione | Solo quando id cambia | Ogni volta che la View appare |
| Raccomandazione Apple | Approccio preferito | Per operazioni sincrone |
.onAppear è ancora utile per operazioni sincrone — ad esempio, logging o configurazione iniziale dell'interfaccia utente. Ma per il caricamento asincrono di dati, richieste di rete, operazioni con database o filesystem, usa .task. È più sicuro e pulito dal punto di vista architetturale.
// ❌ Approccio legacy: Task in .onAppear senza cancellazione
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
.onAppear {
loadTask = Task { await loadData() }
}
.onDisappear { loadTask?.cancel() }
// ✅ Approccio moderno: .task gestisce la cancellazione
func body() { var body: some View { Text("") }
.task { await loadData() }
Il modificatore .task(id:) accetta un parametro aggiuntivo — un identificatore. Quando il valore dell'identificatore cambia, SwiftUI cancella l'attività corrente e ne avvia una nuova con il nuovo identificatore. Questo è ideale per schermate in cui i dati dipendono da un parametro selezionato — ad esempio, un elenco di articoli per categoria o dettagli di un prodotto per 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 {
// gestisci errore
}
}
}
Quando categoryId cambia, SwiftUI cancella la richiesta precedente e ne avvia una nuova. Questo è particolarmente importante per cambi rapidi di categoria — le vecchie richieste non competono con quelle nuove per l'aggiornamento dello stato. Senza .task(id:), dovresti tracciare manualmente i cambiamenti tramite .onChange e gestire il Task manualmente.
Sebbene .task cancelli automaticamente l'attività quando la View scompare, l'operazione asincrona stessa deve verificare cooperativamente la cancellazione. Swift utilizza un modello di cancellazione cooperativa — Task.cancel() non interrompe forzatamente l'esecuzione, ma imposta solo il flag isCancelled. Il codice all'interno dell'attività dovrebbe verificare periodicamente questo flag.
struct LoadingView: View {
@State var progress: Double = 0
var body: some View {
ProgressView(value: progress)
.task {
for i in 0..<100 {
// Verifica cancellazione
try Task.checkCancellation()
await Task.sleep(nanoseconds: 50_000_000)
progress = Double(i + 1) / 100.0
}
}
}
}
Task.checkCancellation() lancia CancellationError se l'attività è stata cancellata. Questo è il modo più semplice di verifica — funziona in qualsiasi contesto async. Un'alternativa è verificare Task.isCancelled manualmente prima di operazioni costose. Per URLSession, le richieste di rete vengono automaticamente cancellate quando l'attività viene cancellata, poiché URLSession supporta Structured Concurrency nativamente.
.task è adatto a molti scenari: dal semplice caricamento JSON a complesse operazioni parallele con TaskGroup. Esaminiamo tre casi d'uso tipici.
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("Caricamento fallito")
}
}
.task {
defer { isLoading = false }
do {
profile = await APIClient().fetchProfile()
} catch {
// profilo rimane nil
}
}
}
}
struct DashboardView: View {
@State var stats: DashboardStats?
var body: some View {
Text("Dashboard")
.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
}
}
}
L'errore più comune è mutare proprietà UI all'interno di .task senza passare al thread principale. Sebbene SwiftUI restituisca automaticamente gli aggiornamenti al thread principale quando si modifica @State in un contesto async, manipolazioni dirette di elementi UIKit all'interno di .task possono causare un crash.
// ❌ Errore: nessuna gestione errori
.task {
let data = await fetchData() // si blocca in caso di errore!
items = data
}
// ✅ Corretto: do/catch
.task {
do {
items = await fetchData()
} catch {
errorMessage = error.localizedDescription
}
}
Domande Frequenti
.task cancella automaticamente l'attività quando la View scompare e supporta Structured Concurrency. Task { } in .onAppear richiede di mantenere manualmente un riferimento all'attività e chiamare cancel() in .onDisappear. .task è anche più facile da leggere — indica esplicitamente che il caricamento dei dati fa parte del ciclo di vita della View.
Sì, per iscriversi a un AsyncSequence o AsyncStream, usa for await value in publisher.values all'interno di .task. Funziona sia con async/await che con Combine tramite l'estensione Publisher.values. Quando la View scompare, l'iterazione terminerà automaticamente e l'iscrizione verrà cancellata.
Sì, se la View all'interno di TabView viene ricreata ad ogni cambio. A partire da iOS 18, TabView può mantenere le Views in memoria — in questo caso .task non si riavvia. Usa .task(id:) con un identificatore di scheda se devi ricaricare i dati ad ogni cambio di scheda.
SwiftUI cancellerà l'attività quando la View scompare. Se la richiesta URLSession era all'interno dell'attività, verrà anch'essa cancellata. Se l'attività non supporta la cancellazione cooperativa (ad esempio, non verifica isCancelled), continuerà a eseguire, ma il suo risultato non verrà applicato allo stato perché la View non esiste più.
Sì, puoi aggiungere più modificatori .task su una singola View. Ognuno crea un'attività indipendente. Questo è utile per separare diverse fonti di dati: un .task per caricare un profilo, un secondo per iscriversi a un WebSocket, un terzo per monitorare la geolocalizzazione.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche