@MainActor to globalny aktor w języku Swift, który gwarantuje wykonanie kodu na głównym wątku. Według Apple Developer, 2024, @MainActor automatyzuje przełączanie na główny wątek podczas pracy z UI, uwalniając programistę od ręcznego wywoływania DispatchQueue.main.async. Adnotacja pojawiła się w Swift 5.5 wraz z systemem async/await.
Najważniejsze
@MainActor — to globalny aktor (global actor) w Swift, który łączy właściwości aktorów z gwarancją wykonania na głównym wątku aplikacji. Jest częścią systemu współbieżności Swift, wprowadzonego w Swift 5.5 wraz z async/await i strukturalną współbieżnością. Adnotacja pozwala programiście nie myśleć o ręcznym przełączaniu wątków i zmniejsza liczbę błędów UI.
Aktor w Swift to typ referencyjny, izolujący swój stan i gwarantujący, że tylko jeden wątek może go modyfikować. @MainActor — to specjalny globalny aktor, którego wykonawcą jest główny wątek. Każdy kod oznaczony @MainActor jest wykonywany na głównym wątku — nawet jeśli został wywołany z zadania tła.
Przed pojawieniem się @MainActor programiści ręcznie przełączali się na główny wątek przez DispatchQueue.main.async. Było to źródło częstych błędów: programiści zapominali o przełączeniu, co prowadziło do crashy z powodu aktualizacji UI nie na głównym wątku. @MainActor rozwiązuje ten problem na poziomie systemu typów.
Źródłem większości błędów w aplikacjach iOS jest UI-niebezpieczeństwo — aktualizacja interfejsu z wątku tła. Apple wbudowało @MainActor w Swift Concurrency, aby przełączanie na główny wątek było automatyczne i sprawdzane przez kompilator, eliminując całą klasę błędów runtime.
Zasada działania @MainActor opiera się na systemie wykonawczym Swift Concurrency. Gdy wątek wywołuje funkcję oznaczoną @MainActor, planista zawiesza ją na bieżącym wykonawcy i wznawia na głównym wątku. Kompilator śledzi granice wywołania i gwarantuje bezpieczeństwo.
Za wykonanie @MainActor odpowiada MainActor.shared — wykonawca powiązany z głównym wątkiem aplikacji. Gdy funkcja asynchroniczna jest oznaczona @MainActor, zawsze wznawia się na tym wykonawcy, niezależnie od tego, na którym wątku zostało uruchomione oryginalne zadanie.
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // bezpiecznie, MainActor gwarantuje główny wątek
}
}
Jeśli funkcja jest oznaczona @MainActor i wywołuje inną funkcję asynchroniczną, domyślnie dziedziczy kontekst aktora. Oznacza to, że wszystkie zagnieżdżone wywołania również są wykonywane na głównym wątku, chyba że określono inaczej. Kompilator śledzi to i zgłasza błąd przy próbie przekazania niezgodnego domknięcia.
Porównanie @MainActor i DispatchQueue.main pomaga zrozumieć, dlaczego nowy mechanizm jest uważany za bezpieczniejszy i wygodniejszy, chociaż oba rozwiązują to samo zadanie — wykonanie kodu na głównym wątku.
@MainActor — to sprawdzenie na poziomie kompilatora. Jeśli próbujesz wywołać funkcję @MainActor z niebezpiecznego kontekstu, kompilator wyświetli ostrzeżenie lub błąd. DispatchQueue.main.async — to wywołanie runtime: kod się skompiluje, ale może spaść w runtime przy próbie aktualizacji UI z wątku tła.
DispatchQueue.main.async dodaje do kolejki blok, który może być wykonany z opóźnieniem. @MainActor z async/await wykonuje bezpośrednie przełączenie wykonawcy bez tworzenia zbędnych domknięć. Zmniejsza to narzut i czyni kod bardziej przewidywalnym pod względem czasu wykonania.
// Stare podejście
DispatchQueue.main.async {
self.updateUI()
}
// Nowe podejście z @MainActor
@MainActor
func updateUI() {
// wykonywane na głównym wątku
self.label.text = "Zaktualizowano"
}
| Kryterium | @MainActor | DispatchQueue.main |
|---|---|---|
| Sprawdzenie | kompilator | runtime |
| Składnia | adnotacja (deklaratywna) | wywołanie (imperatywne) |
| Narzut | niski (przełączenie wykonawcy) | średni (domknięcie + kolejka) |
| Testowalność | wysoka (MainActor.shared można podmienić) | niska (trudno mockować) |
W rzeczywistych projektach iOS @MainActor jest stosowany w warstwach ViewModel, widokach SwiftUI i kontrolerach UIKit. Adnotacja może być zastosowana zarówno do poszczególnych metod, jak i do całego typu.
Oznaczając klasę @MainActor, gwarantujesz, że wszystkie jej metody i właściwości są dostępne tylko na głównym wątku. Jest to szczególnie wygodne dla widoków SwiftUI i klas ObservableObject: wystarczy dodać @MainActor przed class, a wszystkie właściwości @Published są aktualizowane bezpiecznie.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
Podczas pracy ze starym kodem UIKit, gdzie przełączanie wątków było ręczne, można użyć MainActor.run do jawnego przełączenia. Jest to wygodne przy stopniowym przejściu na Swift Concurrency bez przepisywania całej bazy kodu.
await MainActor.run {
self.tableView.reloadData()
}
Pomimo wszystkich zalet, @MainActor ma szereg ograniczeń, które należy uwzględnić przy projektowaniu architektury aplikacji. Zrozumienie granic zastosowania pomaga uniknąć nieprawidłowego użycia.
Jeśli cały łańcuch wywołań jest oznaczony @MainActor, to każda ciężka praca będzie wykonywana na głównym wątku, powodując zawieszanie się UI. Zaleca się oznaczać @MainActor tylko warstwę UI, a logikę biznesową i żądania sieciowe pozostawić w aktorach tła lub globalnym wykonawcy.
Stare API oparte na callbackach (np. URLSession bez async/await) nie obsługują kontekstu aktora. Do integracji wymagane jest opakowanie z CheckedContinuation. Ponadto @MainActor nie jest kompatybilny z performSelector, target-action i innymi nieasynchronicznymi wzorcami UIKit.
Podczas debugowania aplikacji z @MainActor trudniej jest odtworzyć stany wyścigu, ponieważ kompilator zapobiega wielu z nich na etapie budowania, a nie w runtime. Może to jednak stworzyć fałszywe poczucie bezpieczeństwa: nieprawidłowa praca z współdzielonymi obiektami mutable (np. NSCache lub wspólne zmienne globalne) jest nadal możliwa, jeśli nie są oznaczone @MainActor i używane bez jawnej synchronizacji.
@MainActor znacznie upraszcza testowanie logiki UI, ponieważ eliminuje potrzebę ręcznego przełączania wątków w testach. Istnieją jednak cechy, które należy uwzględnić przy pisaniu testów jednostkowych i UI.
W XCTest środowisko wykonawcze automatycznie konfiguruje wykonawcę głównego wątku. Gdy metoda testowa jest uruchamiana na głównym wątku, wywołanie funkcji @MainActor nie wymaga dodatkowych ustawień — są one wykonywane w tym samym kontekście. Do testowania scenariuszy tła użyj MainActor.run wewnątrz Task z jawnym określeniem priorytetu i wykonawcy, osobno sprawdzając, że kod działa poprawnie przy wywołaniu z tła.
Jednym z popularnych podejść jest testowanie ViewModel z @MainActor, gdzie sprawdza się, że właściwości @Published są poprawnie aktualizowane po operacjach asynchronicznych. Dzięki dziedziczeniu kontekstu aktora, wywołanie await wewnątrz testu gwarantuje wykonanie na głównym wątku bez dodatkowych gwarancji DispatchQueue i ręcznego przełączania kontekstów, co upraszcza pisanie testów.
Podczas refaktoringu istniejącego kodu na Swift Concurrency sprawdzaj izolację @MainActor przez kompilator: wszelkie wywołania synchronicznych metod bez @MainActor z kontekstu @MainActor są oznaczone jako błąd. Ta właściwość jest używana do stopniowego przekształcania projektu na async/await: oznaczasz warstwę ViewModel jako @MainActor, a kompilator podświetla wszystkie niebezpieczne wywołania, które należy przenieść do aktorów tła.
Podczas tworzenia mocków dla zależności @MainActor używaj protokołów z async-metodami, które deklarują funkcje asynchroniczne z typami zwracanymi. Pozwala to podmieniać serwisy sieciowe, bazy danych i inne zewnętrzne zależności bez naruszania izolacji aktora. Kompilator sprawdzi, że mock implementuje wszystkie wymagania dotyczące izolacji, zapobiegając przypadkowemu dostępowi do kodu @MainActor z testowych wątków tła.
Podczas synchronicznego testowania kodu @MainActor używaj XCTestExpectation do oczekiwania na zakończenie operacji asynchronicznych. Ustaw oczekiwanie w teście i wykonuj fulfillment wewnątrz domknięcia, które jest wykonywane na głównym wątku. Jeśli test zawiesza się w nieskończoność — najprawdopodobniej wywołanie na głównym wątku nie następuje i należy sprawdzić izolację aktora. Do debugowania kontekstu wykonania warto dodać sprawdzenie Thread.isMainThread wewnątrz kodu testowego.
Często zadawane pytania
Nie, wystarczy oznaczyć tylko metody, które aktualizują UI. Jednak jeśli w klasie jest kilka takich metod, łatwiej dodać @MainActor do całej klasy. Gwarantuje to, że wszystkie jej członki są wykonywane na głównym wątku i upraszcza utrzymanie kodu.
@MainActor — to konkretna instancja globalnego aktora, powiązana z głównym wątkiem. @globalActor — to protokół do tworzenia własnych globalnych aktorów. Na przykład możesz utworzyć @BackgroundActor do wykonywania kodu na wątku tła, jeśli wymaga tego architektura projektu.
Tak, funkcje synchroniczne z @MainActor również są wykonywane na głównym wątku. Jednak główna wartość @MainActor ujawnia się właśnie z async/await, gdy funkcja asynchroniczna automatycznie wznawia się na głównym wątku bez ręcznego przełączania przez DispatchQueue.main.
Task.cancel() działa z zadaniami @MainActor tak samo, jak ze zwykłymi. Zadanie @MainActor może sprawdzać Task.isCancelled lub rzucać CancellationError. Przy anulowaniu główny wątek nie jest blokowany — zadanie po prostu przestaje wykonywać się w najbliższym punkcie zawieszenia.
Kompilator gwarantuje bezpieczeństwo: jeśli wywołujesz funkcję @MainActor z kontekstu tła, kompilator wskaże błąd. W przypadku wywołań asynchronicznych wystarczy oznaczyć wywołujący kod await, a wykonawca sam przełączy się na główny wątek. Dla synchronicznych wymagane jest jawne przełączenie przez MainActor.run.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również