@MainActor je globální actor v jazyce Swift, který garantuje provedení kódu na hlavním vlákně. Podle Apple Developer, 2024, @MainActor automatizuje přepínání na hlavní vlákno při práci s UI, čímž zbavuje vývojáře ručního volání DispatchQueue.main.async. Anotace se objevila ve Swift 5.5 spolu se systémem async/await.
Hlavní body
@MainActor je globální actor (global actor) ve Swift, který kombinuje vlastnosti actorů se zárukou provedení na hlavním vlákně aplikace. Je součástí systému konkurence Swift, představeného ve Swift 5.5 spolu s async/await a strukturovanou konkurencí. Anotace umožňuje vývojáři nemyslet na ruční přepínání vláken a snižuje počet chyb UI.
Actor ve Swift je referenční typ, který izoluje svůj stav a garantuje, že pouze jedno vlákno jej může měnit. @MainActor je speciální globální actor, jehož vykonavatelem je hlavní vlákno. Jakýkoli kód označený @MainActor se provádí na hlavním vlákně — i když byl zavolán z úkolu na pozadí.
Před příchodem @MainActor vývojáři ručně přepínali na hlavní vlákno pomocí DispatchQueue.main.async. To byl zdroj častých chyb: vývojáři zapomínali přepnout, což vedlo k pádům kvůli aktualizaci UI na nehlavním vlákně. @MainActor řeší tento problém na úrovni typového systému.
Zdrojem většiny chyb v iOS aplikacích je nebezpečí UI — aktualizace rozhraní z vlákna na pozadí. Apple zabudoval @MainActor do Swift Concurrency, aby přepínání na hlavní vlákno bylo automatické a kontrolovatelné překladačem, čímž eliminoval celou třídu runtime chyb.
Princip fungování @MainActor je založen na systému provádění Swift Concurrency. Když vlákno zavolá funkci označenou @MainActor, plánovač ji pozastaví na aktuálním vykonavateli a obnoví na hlavním vlákně. Překladač sleduje hranice volání a garantuje bezpečnost.
Za provedení @MainActor odpovídá MainActor.shared — vykonavatel spojený s hlavním vláknem aplikace. Když je asynchronní funkce označena @MainActor, vždy se obnoví na tomto vykonavateli, bez ohledu na to, na kterém vlákně byl původní úkol spuštěn.
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // bezpečně, MainActor garantuje hlavní vlákno
}
}
Pokud je funkce označena @MainActor a volá jinou asynchronní funkci, standardně dědí kontext actoru. To znamená, že všechna vnořená volání se také provádějí na hlavním vlákně, pokud není stanoveno jinak. Překladač to sleduje a při pokusu o předání nekompatibilního uzávěru vydá chybu.
Srovnání @MainActor a DispatchQueue.main pomáhá pochopit, proč je nový mechanismus považován za bezpečnější a pohodlnější, i když oba řeší stejný úkol — provedení kódu na hlavním vlákně.
@MainActor je kontrola na úrovni překladače. Pokud se pokusíte zavolat funkci @MainActor z nebezpečného kontextu, překladač vydá varování nebo chybu. DispatchQueue.main.async je runtime volání: kód se přeloží, ale může spadnout za běhu při pokusu o aktualizaci UI z vlákna na pozadí.
DispatchQueue.main.async přidává do fronty blok, který může být proveden se zpožděním. @MainActor s async/await provádí přímé přepnutí vykonavatele bez vytváření nadbytečných uzávěrů. To snižuje režii a činí kód předvídatelnějším z hlediska doby provádění.
// Starý přístup
DispatchQueue.main.async {
self.updateUI()
}
// Nový přístup s @MainActor
@MainActor
func updateUI() {
// provádí se na hlavním vlákně
self.label.text = "Aktualizováno"
}
| Kritérium | @MainActor | DispatchQueue.main |
|---|---|---|
| Kontrola | překladač | runtime |
| Syntaxe | anotace (deklarativní) | volání (imperativní) |
| Režie | nízká (přepnutí vykonavatele) | střední (uzávěr + fronta) |
| Testovatelnost | vysoká (MainActor.shared lze nahradit) | nízká (obtížně mockovatelné) |
V reálných iOS projektech se @MainActor aplikuje ve vrstvách ViewModel, SwiftUI zobrazeních a UIKit kontrolerech. Anotaci lze aplikovat jak na jednotlivé metody, tak na celý typ.
Označením třídy @MainActor garantujete, že všechny její metody a vlastnosti jsou přístupné pouze na hlavním vlákně. To je zvláště výhodné pro SwiftUI zobrazení a ObservableObject třídy: jednoduše přidáte @MainActor před class a všechny @Published vlastnosti se bezpečně aktualizují.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
Při práci se starým UIKit kódem, kde bylo přepínání vláken ruční, lze použít MainActor.run pro explicitní přepnutí. To je výhodné pro postupný přechod na Swift Concurrency bez přepisování celé kódové základny.
await MainActor.run {
self.tableView.reloadData()
}
Přes všechny výhody má @MainActor řadu omezení, která je třeba zvážit při navrhování architektury aplikace. Pochopení hranic použití pomáhá předejít nesprávnému použití.
Pokud je celý řetězec volání označen @MainActor, jakákoli těžká práce se bude provádět na hlavním vlákně, což způsobí zaseknutí UI. Doporučuje se označit @MainActor pouze UI vrstvu a obchodní logiku a síťové požadavky ponechat v actorích na pozadí nebo globálním vykonavateli.
Stará API založená na callback (např. URLSession bez async/await) nepodporují kontext actoru. Pro integraci je vyžadováno zabalení s CheckedContinuation. Také @MainActor není kompatibilní s performSelector, target-action a jinými neasynchronními vzory UIKit.
Při ladění aplikací s @MainActor je obtížnější reprodukovat závodné stavy, protože překladač jim mnohým předchází ve fázi sestavování, nikoli za běhu. To však může vytvářet falešný pocit bezpečí: nesprávná práce se sdílenými mutable objekty (např. NSCache nebo sdílené globální proměnné) je stále možná, pokud nejsou označeny @MainActor a používány bez explicitní synchronizace.
@MainActor výrazně zjednodušuje testování UI logiky, protože odstraňuje potřebu ručního přepínání vláken v testech. Existují však vlastnosti, které je třeba zvážit při psaní jednotkových testů a UI testů.
V XCTest prostředí provádění automaticky konfiguruje vykonavatele hlavního vlákna. Když se testovací metoda provádí na hlavním vlákně, volání funkcí @MainActor nevyžaduje další nastavení — provádějí se ve stejném kontextu. Pro testování scénářů na pozadí použijte MainActor.run uvnitř Task s explicitním určením priority a vykonavatele, separátně ověřte, že kód funguje správně při volání z pozadí.
Jedním z běžných přístupů je testování ViewModel s @MainActor, kde se ověřuje, že @Published vlastnosti se správně aktualizují po asynchronních operacích. Díky dědičnosti kontextu actoru garantuje volání await uvnitř testu provedení na hlavním vlákně bez dalších DispatchQueue záruk a ručního přepínání kontextů, což zjednodušuje psaní testů.
Při refaktorování existujícího kódu na Swift Concurrency kontrolujte izolaci @MainActor pomocí překladače: jakákoli volání synchronních metod bez @MainActor z kontextu @MainActor jsou označena jako chyba. Tato vlastnost se používá pro postupný přechod projektu na async/await: označíte vrstvu ViewModel jako @MainActor a překladač zvýrazní všechna nebezpečná volání, která je třeba přesunout do actorů na pozadí.
Při vytváření mocků pro závislosti @MainActor použijte protokoly s async metodami, které deklarují asynchronní funkce s návratovými typy. To umožňuje nahrazovat síťové služby, databáze a další externí závislosti bez porušení izolace actoru. Překladač ověří, zda mock splňuje všechny požadavky izolace, čímž brání náhodnému přístupu k @MainActor kódu z testovacích vláken na pozadí.
Při synchronním testování @MainActor kódu použijte XCTestExpectation pro čekání na dokončení asynchronních operací. Nastavte očekávání v testu a proveďte fulfillment uvnitř uzávěru, který se provádí na hlavním vlákně. Pokud test nekonečně visí — volání na hlavním vlákně se pravděpodobně neprovádí a je třeba zkontrolovat izolaci actoru. Pro ladění kontextu provádění je užitečné přidat kontrolu Thread.isMainThread uvnitř testovacího kódu.
Často kladené otázky
Ne, stačí označit pouze metody, které aktualizují UI. Pokud je však ve třídě několik takových metod, je jednodušší přidat @MainActor k celé třídě. To garantuje, že všechny její členy se provádějí na hlavním vlákně a zjednodušuje údržbu kódu.
@MainActor je konkrétní instance globálního actoru, připoutaná k hlavnímu vláknu. @globalActor je protokol pro vytváření vlastních globálních actorů. Například můžete vytvořit @BackgroundActor pro provádění kódu na vlákně na pozadí, pokud to architektura projektu vyžaduje.
Ano, synchronní funkce s @MainActor se také provádějí na hlavním vlákně. Hlavní hodnota @MainActor se však odhaluje právě s async/await, když se asynchronní funkce automaticky obnoví na hlavním vlákně bez ručního přepnutí přes DispatchQueue.main.
Task.cancel() pracuje s úkoly @MainActor stejně jako s běžnými úkoly. Úkol @MainActor může kontrolovat Task.isCancelled nebo vyhodit CancellationError. Při zrušení není hlavní vlákno blokováno — úkol jednoduše zastaví provádění v nejbližším bodě pozastavení.
Překladač garantuje bezpečnost: pokud zavoláte funkci @MainActor z kontextu na pozadí, překladač označí chybu. Pro asynchronní volání stačí označit volající kód await a vykonavatel sám přepne na hlavní vlákno. Pro synchronní volání je vyžadováno explicitní přepnutí pomocí MainActor.run.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také