@MainActor è un attore globale nel linguaggio Swift che garantisce l'esecuzione del codice sul thread principale. Secondo Apple Developer, 2024, @MainActor automatizza il passaggio al thread principale quando si lavora con l'UI, liberando lo sviluppatore dalla chiamata manuale a DispatchQueue.main.async. L'annotazione è apparsa in Swift 5.5 insieme al sistema async/await.
Punti chiave
@MainActor è un attore globale in Swift che combina le proprietà degli attori con la garanzia di esecuzione sul thread principale dell'applicazione. Fa parte del sistema di concorrenza Swift introdotto in Swift 5.5 insieme ad async/await e alla concorrenza strutturata. L'annotazione permette allo sviluppatore di non preoccuparsi del passaggio manuale dei thread e riduce il numero di errori UI.
Un attore in Swift è un tipo per riferimento che isola il proprio stato e garantisce che un solo thread possa modificarlo. @MainActor è un attore globale speciale il cui esecutore è il thread principale. Qualsiasi codice marcato con @MainActor viene eseguito sul thread principale — anche se chiamato da un'attività in background.
Prima di @MainActor, gli sviluppatori passavano manualmente al thread principale tramite DispatchQueue.main.async. Questa era una fonte frequente di errori: gli sviluppatori dimenticavano di passare, causando crash dovuti ad aggiornamenti UI non sul thread principale. @MainActor risolve questo problema a livello di sistema dei tipi.
La fonte della maggior parte dei bug nelle applicazioni iOS è l'insicurezza dell'UI — aggiornare l'interfaccia da un thread secondario. Apple ha integrato @MainActor in Swift Concurrency per rendere il passaggio al thread principale automatico e verificabile dal compilatore, eliminando un'intera classe di errori runtime.
Il principio di funzionamento di @MainActor si basa sul sistema di esecuzione di Swift Concurrency. Quando un thread chiama una funzione marcata con @MainActor, lo scheduler la sospende sull'esecutore corrente e la riprende sul thread principale. Il compilatore traccia i confini della chiamata e garantisce la sicurezza.
L'esecuzione di @MainActor è gestita da MainActor.shared — un esecutore associato al thread principale dell'applicazione. Quando una funzione asincrona è marcata con @MainActor, riprende sempre su questo esecutore, indipendentemente dal thread su cui è stata avviata l'attività originale.
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // in modo sicuro, MainActor garantisce il thread principale
}
}
Se una funzione è marcata con @MainActor e chiama un'altra funzione asincrona, eredita il contesto dell'attore per impostazione predefinita. Ciò significa che tutte le chiamate annidate vengono eseguite anch'esse sul thread principale, salvo diversa indicazione. Il compilatore traccia questo e genera un errore quando si tenta di passare una chiusura incoerente.
Il confronto tra @MainActor e DispatchQueue.main aiuta a capire perché il nuovo meccanismo è considerato più sicuro e conveniente, sebbene entrambi risolvano lo stesso compito — eseguire codice sul thread principale.
@MainActor è un controllo a livello di compilatore. Se si tenta di chiamare una funzione @MainActor da un contesto non sicuro, il compilatore emetterà un avviso o un errore. DispatchQueue.main.async è una chiamata runtime: il codice verrà compilato ma potrebbe crashare a runtime quando si tenta di aggiornare l'UI da un thread secondario.
DispatchQueue.main.async aggiunge un blocco alla coda che può essere eseguito con ritardo. @MainActor con async/await esegue un cambio diretto di esecutore senza creare chiusure non necessarie. Ciò riduce l'overhead e rende il tempo di esecuzione più prevedibile.
// Approccio vecchio
DispatchQueue.main.async {
self.updateUI()
}
// Nuovo approccio con @MainActor
@MainActor
func updateUI() {
// viene eseguito sul thread principale
self.label.text = "Aggiornato"
}
| Criterio | @MainActor | DispatchQueue.main |
|---|---|---|
| Controllo | compilatore | runtime |
| Sintassi | annotazione (dichiarativa) | chiamata (imperativa) |
| Overhead | basso (cambio esecutore) | medio (chiusura + coda) |
| Testabilità | alta (MainActor.shared sostituibile) | bassa (difficile da mockare) |
Nei progetti iOS reali, @MainActor viene utilizzato nei livelli ViewModel, nelle viste SwiftUI e nei controller UIKit. L'annotazione può essere applicata sia a singoli metodi che all'intero tipo.
Marcando una classe con @MainActor, si garantisce che tutti i suoi metodi e proprietà siano accessibili solo sul thread principale. Questo è particolarmente comodo per le viste SwiftUI e le classi ObservableObject: basta aggiungere @MainActor prima della classe e tutte le proprietà @Published vengono aggiornate in modo sicuro.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
Quando si lavora con codice UIKit legacy dove il cambio di thread era manuale, è possibile utilizzare MainActor.run per il cambio esplicito. Questo è comodo per una transizione incrementale a Swift Concurrency senza riscrivere l'intera base di codice.
await MainActor.run {
self.tableView.reloadData()
}
Nonostante tutti i suoi vantaggi, @MainActor presenta una serie di limitazioni importanti da considerare quando si progetta l'architettura dell'applicazione. Comprendere i limiti di applicabilità aiuta a evitare un uso scorretto.
Se l'intera catena di chiamate è marcata con @MainActor, allora qualsiasi lavoro pesante verrà eseguito sul thread principale, causando blocchi dell'UI. Si consiglia di marcare solo il livello UI con @MainActor, lasciando la logica di business e le richieste di rete in attori in background o nell'esecutore globale.
Le vecchie API basate su callback (ad esempio, URLSession senza async/await) non supportano il contesto dell'attore. L'integrazione richiede un wrapper con CheckedContinuation. Inoltre, @MainActor non è compatibile con performSelector, target-action e altri pattern non asincroni di UIKit.
Durante il debug di applicazioni con @MainActor, è più difficile riprodurre condizioni di gara perché il compilatore ne previene molte in fase di compilazione anziché in fase di esecuzione. Tuttavia, ciò può creare un falso senso di sicurezza: un lavoro errato con oggetti mutabili condivisi (ad esempio NSCache o variabili globali condivise) è ancora possibile se non sono marcati con @MainActor e vengono utilizzati senza sincronizzazione esplicita.
@MainActor semplifica notevolmente il test della logica UI poiché elimina la necessità di cambiare manualmente i thread nei test. Tuttavia, ci sono particolarità da considerare quando si scrivono test unitari e test UI.
In XCTest, l'ambiente di test configura automaticamente l'esecutore del thread principale. Quando un metodo di test viene eseguito sul thread principale, chiamare funzioni @MainActor non richiede configurazione aggiuntiva — vengono eseguite nello stesso contesto. Per testare scenari in background, utilizzare MainActor.run all'interno di un Task con priorità ed esecutore espliciti, verificando separatamente che il codice funzioni correttamente quando chiamato dallo sfondo.
Un approccio comune è testare ViewModel con @MainActor, dove si verifica che le proprietà @Published si aggiornino correttamente dopo operazioni asincrone. Grazie all'ereditarietà del contesto dell'attore, chiamare await all'interno del test garantisce l'esecuzione sul thread principale senza garanzie aggiuntive di DispatchQueue o cambio manuale di contesto, semplificando la scrittura dei test.
Durante il refactoring di codice esistente verso Swift Concurrency, verificare l'isolamento di @MainActor tramite il compilatore: qualsiasi chiamata a metodi sincroni senza @MainActor da un contesto @MainActor viene contrassegnata come errore. Questa proprietà viene utilizzata per migrare gradualmente un progetto verso async/await: si marca il livello ViewModel come @MainActor e il compilatore evidenzia tutte le chiamate non sicure che devono essere spostate in attori in background.
Quando si creano mock per dipendenze @MainActor, utilizzare protocolli con metodi async che dichiarano funzioni asincrone con tipi di ritorno. Ciò consente di sostituire servizi di rete, database e altre dipendenze esterne senza rompere l'isolamento dell'attore. Il compilatore verifica che il mock implementi tutti i requisiti di isolamento, impedendo l'accesso accidentale al codice @MainActor da thread di test in background.
Quando si testa codice @MainActor in modo sincrono, utilizzare XCTestExpectation per attendere il completamento delle operazioni asincrone. Impostare l'aspettativa nel test e chiamare fulfillment all'interno di una chiusura che viene eseguita sul thread principale. Se il test si blocca indefinitamente — probabilmente la chiamata sul thread principale non sta avvenendo ed è necessario verificare l'isolamento dell'attore. Per eseguire il debug del contesto di esecuzione, è utile aggiungere un controllo Thread.isMainThread all'interno del codice di test.
Domande frequenti
No, è sufficiente marcare solo i metodi che aggiornano l'UI. Tuttavia, se una classe ha diversi metodi di questo tipo, è più semplice aggiungere @MainActor all'intera classe. Ciò garantisce che tutti i suoi membri vengano eseguiti sul thread principale e semplifica la manutenzione del codice.
@MainActor è un'istanza specifica di un attore globale legata al thread principale. @globalActor è un protocollo per creare i propri attori globali. Ad esempio, è possibile creare un @BackgroundActor per eseguire codice su un thread secondario se l'architettura del progetto lo richiede.
Sì, le funzioni sincrone con @MainActor vengono eseguite anch'esse sul thread principale. Tuttavia, il valore principale di @MainActor si rivela con async/await, quando una funzione asincrona riprende automaticamente sul thread principale senza cambio manuale tramite DispatchQueue.main.
Task.cancel() funziona con le attività @MainActor allo stesso modo che con quelle normali. Un'attività @MainActor può verificare Task.isCancelled o lanciare CancellationError. All'annullamento, il thread principale non viene bloccato — l'attività interrompe semplicemente l'esecuzione al punto di sospensione più vicino.
Il compilatore garantisce la sicurezza: se si chiama una funzione @MainActor da un contesto secondario, il compilatore segnalerà l'errore. Per le chiamate asincrone, è sufficiente marcare il codice chiamante con await e l'esecutore passerà automaticamente al thread principale. Per le chiamate sincrone, è necessario un cambio esplicito tramite MainActor.run.
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