@MainActor este un actor global în limbajul Swift care garantează executarea codului pe firul principal. Conform Apple Developer, 2024, @MainActor automatizează comutarea pe firul principal la lucrul cu UI, eliberând dezvoltatorul de apelul manual DispatchQueue.main.async. Adnotarea a apărut în Swift 5.5 odată cu sistemul async/await.
Principalele puncte
@MainActor este un actor global (global actor) în Swift care combină proprietățile actorilor cu garanția executării pe firul principal al aplicației. Face parte din sistemul de concurență Swift, introdus în Swift 5.5 odată cu async/await și concurența structurată. Adnotarea permite dezvoltatorului să nu se gândească la comutarea manuală a firelor și reduce numărul de erori UI.
Actorul în Swift este un tip referențial care își izolează starea și garantează că doar un singur fir o poate modifica. @MainActor este un actor global special al cărui executant este firul principal. Orice cod marcat cu @MainActor se execută pe firul principal — chiar dacă a fost apelat dintr-o sarcină de fundal.
Înainte de apariția @MainActor, dezvoltatorii comutau manual pe firul principal prin DispatchQueue.main.async. Aceasta era o sursă de erori frecvente: dezvoltatorii uitau să comute, ceea ce ducea la crash-uri din cauza actualizării UI pe un fir neprincipal. @MainActor rezolvă această problemă la nivelul sistemului de tipuri.
Sursa majorității bug-urilor în aplicațiile iOS este nesiguranța UI — actualizarea interfeței dintr-un fir de fundal. Apple a încorporat @MainActor în Swift Concurrency pentru a face comutarea pe firul principal automată și verificabilă de compilator, eliminând o întreagă clasă de erori de runtime.
Principiul de funcționare al @MainActor se bazează pe sistemul de execuție Swift Concurrency. Când un fir apelează o funcție marcată cu @MainActor, planificatorul o suspendă pe executantul curent și o reia pe firul principal. Compilatorul urmărește granițele apelului și garantează siguranța.
Pentru executarea @MainActor răspunde MainActor.shared — executantul asociat cu firul principal al aplicației. Când o funcție asincronă este marcată cu @MainActor, ea întotdeauna se reia pe acest executant, indiferent de firul pe care a fost lansată sarcina originală.
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // în siguranță, MainActor garantează firul principal
}
}
Dacă o funcție este marcată cu @MainActor și apelează o altă funcție asincronă, în mod implicit moștenește contextul actorului. Aceasta înseamnă că toate apelurile imbricate se execută și ele pe firul principal, cu excepția cazului în care se specifică altfel. Compilatorul urmărește acest lucru și emite o eroare la încercarea de a transmite un closure necorespunzător.
Comparația @MainActor și DispatchQueue.main ajută la înțelegerea de ce noul mecanism este considerat mai sigur și mai convenabil, deși ambele rezolvă aceeași sarcină — executarea codului pe firul principal.
@MainActor este o verificare la nivel de compilator. Dacă încem să apelezi o funcție @MainActor dintr-un context nesigur, compilatorul va emite un avertisment sau o eroare. DispatchQueue.main.async este un apel de runtime: codul se compilează, dar poate pica în runtime la încercarea de a actualiza UI dintr-un fir de fundal.
DispatchQueue.main.async adaugă în coadă un bloc care poate fi executat cu întârziere. @MainActor cu async/await efectuează o comutare directă a executantului fără a crea closure-uri suplimentare. Aceasta reduce overhead-ul și face codul mai predictibil din punctul de vedere al timpului de execuție.
// Abordarea veche
DispatchQueue.main.async {
self.updateUI()
}
// Abordarea nouă cu @MainActor
@MainActor
func updateUI() {
// se execută pe firul principal
self.label.text = "Actualizat"
}
| Criteriu | @MainActor | DispatchQueue.main |
|---|---|---|
| Verificare | compilator | runtime |
| Sintaxă | adnotare (declarativă) | apel (imperativ) |
| Overhead | scăzut (comutare executant) | mediu (closure + coadă) |
| Testabilitate | ridicată (MainActor.shared poate fi înlocuit) | scăzută (greu de mockuit) |
În proiectele iOS reale, @MainActor este aplicat în straturile ViewModel, vizualizările SwiftUI și controlerele UIKit. Adnotarea poate fi aplicată atât metodelor individuale, cât și întregului tip.
Marcând o clasă cu @MainActor, garantezi că toate metodele și proprietățile sale sunt accesibile doar pe firul principal. Acest lucru este deosebit de convenabil pentru vizualizările SwiftUI și clasele ObservableObject: adaugi pur și simplu @MainActor înainte de class, iar toate proprietățile @Published se actualizează în siguranță.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
La lucrul cu codul vechi UIKit, unde comutarea firelor era manuală, se poate folosi MainActor.run pentru comutarea explicită. Acest lucru este convenabil pentru tranziția treptată la Swift Concurrency fără a rescrie întreaga bază de cod.
await MainActor.run {
self.tableView.reloadData()
}
În ciuda tuturor avantajelor, @MainActor are o serie de limitări care trebuie luate în considerare la proiectarea arhitecturii aplicației. Înțelegerea granițelor de aplicare ajută la evitarea utilizării incorecte.
Dacă întreaga lanț de apeluri este marcat cu @MainActor, orice muncă grea se va executa pe firul principal, provocând înghețarea UI. Se recomandă să se marcheze cu @MainActor doar stratul UI, iar logica de afaceri și cererile de rețea să rămână în actorii de fundal sau executantul global.
API-urile vechi bazate pe callback (de exemplu, URLSession fără async/await) nu suportă contextul actorului. Pentru integrare este necesară o ambalsare cu CheckedContinuation. De asemenea, @MainActor nu este compatibil cu performSelector, target-action și alte pattern-uri neasincrone UIKit.
La debugarea aplicațiilor cu @MainActor este mai dificil de reprodus stările de cursă, deoarece compilatorul previne multe dintre ele în faza de construire, nu în runtime. Totuși, aceasta poate crea un fals sentiment de siguranță: lucrul incorect cu obiecte mutable partajate (de exemplu, NSCache sau variabile globale comune) este încă posibil dacă nu sunt marcate cu @MainActor și utilizate fără sincronizare explicită.
@MainActor simplifică semnificativ testarea logicii UI, deoarece elimină necesitatea comutării manuale a firelor în teste. Totuși, există caracteristici care trebuie luate în considerare la scrierea testelor unitare și a testelor UI.
În XCTest, mediul de execuție configurează automat executantul firului principal. Când metoda de test se execută pe firul principal, apelarea funcțiilor @MainActor nu necesită setări suplimentare — ele se execută în același context. Pentru testarea scenariilor de fundal, folosește MainActor.run în interiorul Task cu specificarea explicită a priorității și executantului, verificând separat că codul funcționează corect la apelarea din fundal.
Una dintre abordările comune este testarea ViewModel cu @MainActor, unde se verifică că proprietățile @Published se actualizează corect după operații asincrone. Datorită moștenirii contextului actorului, apelul await în interiorul testului garantează executarea pe firul principal fără garanții suplimentare DispatchQueue și comutare manuală a contextelor, ceea ce simplifică scrierea testelor.
La refactorizarea codului existent la Swift Concurrency, verifică izolarea @MainActor prin compilator: orice apeluri de metode sincrone fără @MainActor din contextul @MainActor sunt marcate ca eroare. Această proprietate este utilizată pentru tranziția treptată a proiectului la async/await: marchezi stratul ViewModel ca @MainActor, iar compilatorul evidențioză toate apelurile nesigure care trebuie mutate în actorii de fundal.
La crearea mock-urilor pentru dependențele @MainActor, folosește protocoale cu metode async care declară funcții asincrone cu tipuri de returnare. Aceasta permite înlocuirea serviciilor de rețea, bazelor de date și a altor dependențe externe fără a încălca izolarea actorului. Compilatorul va verifica dacă mock-ul îndeplinește toate cerințele de izolare, prevenind accesul accidental la codul @MainActor din firele de test de fundal.
La testarea sincronă a codului @MainActor, folosește XCTestExpectation pentru a aștepta finalizarea operațiilor asincrone. Setează așteptarea în test și execută fulfillment în interiorul closure-ului care se execută pe firul principal. Dacă testul se blochează la nesfârșit — cel mai probabil, apelul pe firul principal nu are loc și trebuie verificată izolarea actorului. Pentru debugarea contextului de execuție, este util să adaugi verificarea Thread.isMainThread în interiorul codului de test.
Întrebări frecvente
Nu, este suficient să marchezi doar metodele care actualizează UI. Totuși, dacă în clasă există mai multe astfel de metode, este mai simplu să adaugi @MainActor la întreaga clasă. Aceasta garantează că toți membrii săi se execută pe firul principal și simplifică întreținerea codului.
@MainActor este o instanță concretă a actorului global, legată de firul principal. @globalActor este un protocol pentru crearea propriilor actori globali. De exemplu, poți crea @BackgroundActor pentru executarea codului pe un fir de fundal, dacă arhitectura proiectului o cere.
Da, funcțiile sincrone cu @MainActor se execută de asemenea pe firul principal. Cu toate acestea, valoarea principală a @MainActor se dezvăluie tocmai cu async/await, când funcția asincronă se reia automat pe firul principal fără comutare manuală prin DispatchQueue.main.
Task.cancel() funcționează cu sarcinile @MainActor la fel ca și cu cele obișnuite. O sarcină @MainActor poate verifica Task.isCancelled sau arunca CancellationError. La anulare, firul principal nu este blocat — sarcina pur și simplu încetează execuția la cel mai apropiat punct de suspendare.
Compilatorul garantează siguranța: dacă apelezi o funcție @MainActor dintr-un context de fundal, compilatorul va indica eroarea. Pentru apelurile asincrone, este suficient să marchezi codul apelant cu await, iar executantul va comuta singur pe firul principal. Pentru cele sincrone, este necesară comutarea explicită prin MainActor.run.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și