@MainActor is een wereldwijde actor in de programmeertaal Swift die de uitvoering van code op de hoofdthread garandeert. Volgens Apple Developer, 2024 automatiseert @MainActor het schakelen naar de hoofdthread bij het werken met UI, waardoor de ontwikkelaar wordt verlost van het handmatig aanroepen van DispatchQueue.main.async. De annotatie verscheen in Swift 5.5 samen met het async/await-systeem.
Belangrijkste punten
@MainActor is een wereldwijde actor (global actor) in Swift die de eigenschappen van actoren combineert met de garantie van uitvoering op de hoofdthread van de applicatie. Het maakt deel uit van het Swift-concurrentiesysteem, geïntroduceerd in Swift 5.5 samen met async/await en gestructureerde concurrentie. De annotatie stelt de ontwikkelaar in staat niet na te denken over het handmatig schakelen van threads en vermindert het aantal UI-fouten.
Een actor in Swift is een referentietype dat zijn toestand isoleert en garandeert dat slechts één thread deze kan wijzigen. @MainActor is een speciale wereldwijde actor waarvan de uitvoerder de hoofdthread is. Alle code gemarkeerd met @MainActor wordt uitgevoerd op de hoofdthread — zelfs als deze is aangeroepen vanuit een achtergrondtaak.
Vóór de komst van @MainActor schakelden ontwikkelaars handmatig naar de hoofdthread via DispatchQueue.main.async. Dit was een bron van veelvuldige fouten: ontwikkelaars vergaten te schakelen, wat leidde tot crashes door UI-updates op een niet-hoofdthread. @MainActor lost dit probleem op op het niveau van het typesysteem.
De bron van de meeste bugs in iOS-applicaties is UI-onveiligheid — het updaten van de interface vanuit een achtergrondthread. Apple heeft @MainActor ingebouwd in Swift Concurrency om het schakelen naar de hoofdthread automatisch en controleerbaar door de compiler te maken, waarmee een hele klasse runtime-fouten wordt geëlimineerd.
Het werkingsprincipe van @MainActor is gebaseerd op het uitvoeringssysteem van Swift Concurrency. Wanneer een thread een functie aanroept die is gemarkeerd met @MainActor, onderbreekt de planner deze op de huidige uitvoerder en hervat deze op de hoofdthread. De compiler volgt de aanroepgrenzen en garandeert veiligheid.
Voor de uitvoering van @MainActor is MainActor.shared verantwoordelijk — de uitvoerder die is gekoppeld aan de hoofdthread van de applicatie. Wanneer een asynchrone functie is gemarkeerd met @MainActor, wordt deze altijd hervat op deze uitvoerder, ongeacht op welke thread de oorspronkelijke taak is gestart.
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // veilig, MainActor garandeert de hoofdthread
}
}
Als een functie is gemarkeerd met @MainActor en een andere asynchrone functie aanroept, erft deze standaard de actorcontext. Dit betekent dat alle geneste aanroepen ook worden uitgevoerd op de hoofdthread, tenzij anders aangegeven. De compiler volgt dit en geeft een fout bij een poging om een incompatibele closure door te geven.
Vergelijking van @MainActor en DispatchQueue.main helpt te begrijpen waarom het nieuwe mechanisme als veiliger en handiger wordt beschouwd, hoewel beide dezelfde taak oplossen — uitvoering van code op de hoofdthread.
@MainActor is een controle op compilerniveau. Als je probeert een @MainActor-functie aan te roepen vanuit een onveilige context, geeft de compiler een waarschuwing of fout. DispatchQueue.main.async is een runtime-aanroep: de code compileert, maar kan crashen in runtime bij een poging om UI te updaten vanuit een achtergrondthread.
DispatchQueue.main.async voegt een blok toe aan de wachtrij dat met vertraging kan worden uitgevoerd. @MainActor met async/await voert een directe schakeling van de uitvoerder uit zonder extra closures te creëren. Dit vermindert overhead en maakt de code voorspelbaarder qua uitvoeringstijd.
// Oude aanpak
DispatchQueue.main.async {
self.updateUI()
}
// Nieuwe aanpak met @MainActor
@MainActor
func updateUI() {
// wordt uitgevoerd op de hoofdthread
self.label.text = "Bijgewerkt"
}
| Criterium | @MainActor | DispatchQueue.main |
|---|---|---|
| Controle | compiler | runtime |
| Syntaxis | annotatie (declaratief) | aanroep (imperatief) |
| Overhead | laag (uitvoerder schakelen) | gemiddeld (closure + wachtrij) |
| Testbaarheid | hoog (MainActor.shared kan worden vervangen) | laag (moeilijk te mocken) |
In echte iOS-projecten wordt @MainActor toegepast in ViewModel-lagen, SwiftUI-weergaven en UIKit-controllers. De annotatie kan zowel op afzonderlijke methoden als op het hele type worden toegepast.
Door een klasse te markeren met @MainActor garandeer je dat al zijn methoden en eigenschappen alleen toegankelijk zijn op de hoofdthread. Dit is vooral handig voor SwiftUI-weergaven en ObservableObject-klassen: je voegt gewoon @MainActor toe vóór class, en alle @Published-eigenschappen worden veilig geüpdatet.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
Bij het werken met oude UIKit-code, waar het schakelen van threads handmatig was, kan MainActor.run worden gebruikt voor expliciet schakelen. Dit is handig voor een geleidelijke overgang naar Swift Concurrency zonder de hele codebase te herschrijven.
await MainActor.run {
self.tableView.reloadData()
}
Ondanks alle voordelen heeft @MainActor een aantal beperkingen die in overweging moeten worden genomen bij het ontwerpen van de applicatiearchitectuur. Inzicht in de toepassingsgrenzen helpt verkeerd gebruik te voorkomen.
Als de hele aanroepketen is gemarkeerd met @MainActor, wordt elk zwaar werk uitgevoerd op de hoofdthread, wat UI-bevriezing veroorzaakt. Het wordt aanbevolen om alleen de UI-laag te markeren met @MainActor en bedrijfslogica en netwerkverzoeken in achtergrondactoren of de globale uitvoerder te laten.
Oude callback-gebaseerde API's (bijv. URLSession zonder async/await) ondersteunen de actorcontext niet. Voor integratie is een wrapper met CheckedContinuation vereist. Ook is @MainActor niet compatibel met performSelector, target-action en andere niet-asynchrone UIKit-patronen.
Bij het debuggen van applicaties met @MainActor is het moeilijker om race-omstandigheden te reproduceren, omdat de compiler er veel van voorkomt in de bouwfase, niet in runtime. Dit kan echter een vals gevoel van veiligheid creëren: onjuist werken met gedeelde mutable objecten (bijv. NSCache of gedeelde globale variabelen) is nog steeds mogelijk als ze niet zijn gemarkeerd met @MainActor en zonder expliciete synchronisatie worden gebruikt.
@MainActor vereenvoudigt het testen van UI-logica aanzienlijk, omdat het de noodzaak elimineert om handmatig threads te schakelen in tests. Er zijn echter kenmerken waarmee rekening moet worden gehouden bij het schrijven van unittesten en UI-tests.
In XCTest configureert de uitvoeringsomgeving automatisch de uitvoerder van de hoofdthread. Wanneer de testmethode op de hoofdthread wordt uitgevoerd, vereist het aanroepen van @MainActor-functies geen extra configuratie — ze worden in dezelfde context uitgevoerd. Voor het testen van achtergrondscenario's gebruik je MainActor.run binnen Task met expliciete specificatie van prioriteit en uitvoerder, waarbij je afzonderlijk controleert dat de code correct werkt bij aanroep vanuit de achtergrond.
Een van de gebruikelijke benaderingen is het testen van ViewModel met @MainActor, waarbij wordt gecontroleerd dat @Published-eigenschappen correct worden bijgewerkt na asynchrone bewerkingen. Dankzij overerving van de actorcontext garandeert de await-aanroep binnen de test uitvoering op de hoofdthread zonder extra DispatchQueue-garanties en handmatige contextomschakeling, wat het schrijven van tests vereenvoudigt.
Bij het refactoren van bestaande code naar Swift Concurrency controleer je de @MainActor-isolatie via de compiler: elke aanroep van synchrone methoden zonder @MainActor vanuit de @MainActor-context wordt gemarkeerd als een fout. Deze eigenschap wordt gebruikt voor de geleidelijke overgang van het project naar async/await: je markeert de ViewModel-laag als @MainActor, en de compiler markeert alle onveilige aanroepen die naar achtergrondactoren moeten worden verplaatst.
Bij het maken van mocks voor @MainActor-afhankelijkheden gebruik je protocollen met async-methoden die asynchrone functies met retourtypen declareren. Dit maakt het mogelijk om netwerkdiensten, databases en andere externe afhankelijkheden te vervangen zonder de actorisolatie te schenden. De compiler controleert of de mock voldoet aan alle isolatievereisten, waardoor onbedoelde toegang tot @MainActor-code vanuit achtergrondtestthreads wordt voorkomen.
Bij het synchroon testen van @MainActor-code gebruik je XCTestExpectation om te wachten op voltooiing van asynchrone bewerkingen. Stel de verwachting in de test in en voer fulfillment uit binnen de closure die op de hoofdthread wordt uitgevoerd. Als de test oneindig blijft hangen — vindt de aanroep op de hoofdthread waarschijnlijk niet plaats en moet de actorisolatie worden gecontroleerd. Voor het debuggen van de uitvoeringscontext is het nuttig om Thread.isMainThread-controle toe te voegen in de testcode.
Veelgestelde vragen
Nee, het is voldoende om alleen de methoden te markeren die de UI updaten. Als er echter meerdere van dergelijke methoden in de klasse zijn, is het eenvoudiger om @MainActor aan de hele klasse toe te voegen. Dit garandeert dat al zijn leden op de hoofdthread worden uitgevoerd en vereenvoudigt het onderhoud van de code.
@MainActor is een concrete instantie van de wereldwijde actor, gekoppeld aan de hoofdthread. @globalActor is een protocol voor het maken van je eigen wereldwijde actoren. Je kunt bijvoorbeeld @BackgroundActor maken voor het uitvoeren van code op een achtergrondthread, als de projectarchitectuur dit vereist.
Ja, synchrone functies met @MainActor worden ook op de hoofdthread uitgevoerd. De belangrijkste waarde van @MainActor komt echter tot uiting met async/await, wanneer de asynchrone functie automatisch wordt hervat op de hoofdthread zonder handmatige schakeling via DispatchQueue.main.
Task.cancel() werkt met @MainActor-taken hetzelfde als met gewone taken. Een @MainActor-taak kan Task.isCancelled controleren of CancellationError gooien. Bij annulering wordt de hoofdthread niet geblokkeerd — de taak stopt eenvoudig met uitvoeren op het dichtstbijzijnde opschortingspunt.
De compiler garandeert veiligheid: als je een @MainActor-functie aanroept vanuit een achtergrondcontext, geeft de compiler een fout aan. Voor asynchrone aanroepen is het voldoende om de aanroepende code met await te markeren, en de uitvoerder schakelt zelf naar de hoofdthread. Voor synchrone aanroepen is expliciete schakeling via MainActor.run vereist.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook