@MainActor је глобални актор у језику Swift који гарантује извршавање кода на главној нити. Према подацима Apple Developer, 2024, @MainActor аутоматизује пребацивање на главну нит при раду са UI-јем, ослобађајући програмера ручног позива DispatchQueue.main.async. Анотација се појавила у Swift 5.5 заједно са системом async/await.
Главно
@MainActor је глобални актор (global actor) у Swift-у који обједињује особине актора и гаранцију извршавања на главној нити апликације. Он је део Swift система за паралелизам, представљеног у Swift 5.5 заједно са async/await-ом и структурисаном конкурентношћу. Анотација омогућава програмеру да не размишља о ручном пребацивању нити и смањује број UI грешака.
Актор у Swift-у је референтни тип који изолује своје стање и гарантује да само једна нит може да га мења. @MainActor је посебан глобални актор, чији је извршилац главна нит. Било који код означен са @MainActor извршава се на главној нити — чак и ако је позван из позадинске задатке.
Пре појаве @MainActor-а, програмери су се ручно пребацивали на главну нит преко DispatchQueue.main.async. То је био извор честих грешака: програмери су заборављали да се пребаце, што је доводило до крашова због ажурирања UI-ја ван главне нити. @MainActor решава овај проблем на нивоу система типова.
Извор већине багова у iOS апликацијама је UI несигурност — ажурирање интерфејса из позадинске нити. Епл је уградио @MainActor у Swift Concurrency да би пребацивање на главну нит учинио аутоматским и проверљивим од стране компајлера, елиминишући читаву класу runtime грешака.
Принцип рада @MainActor-а заснован је на систему извршавања Swift Concurrency. Када нит позове функцију означену са @MainActor, планирач је суспендује на тренутном извршиоцу и наставља на главној нити. Компајлер прати границе позива и гарантује сигурност.
За извршавање @MainActor-а задужен је MainActor.shared — извршилац повезан са главном нити апликације. Када је асинхрона функција означена са @MainActor, она се увек наставља на овом извршиоцу, без обзира на то на којој нити је покренут изворни задатак.
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // безбедно, MainActor гарантује главну нит
}
}
Ако је функција означена са @MainActor и позива другу асинхрону функцију, подразумевано наслеђује контекст актора. То значи да се сви угнежђени позиви такође извршавају на главној нити, осим ако није другачије наведено. Компајлер то прати и издаје грешку при покушају прослеђивања недоследног замицања (closure).
Поређење @MainActor-а и DispatchQueue.main помаже да се разуме зашто се нови механизам сматра сигурнијим и практичнијим, иако оба решавају исти проблем — извршавање кода на главној нити.
@MainActor је провера на нивоу компајлера. Ако покушате да позовете @MainActor функцију из несигурног контекста, компајлер ће издати упозорење или грешку. DispatchQueue.main.async је runtime позив: код ће се компајлирати, али може да падне у рантајму при покушају ажурирања UI-ја из позадинске нити.
DispatchQueue.main.async додаје у ред блок који може бити извршен са закашњењем. @MainActor са async/await-ом извршава директно пребацивање извршиоца без стварања сувишних замицања. То смањује додатне трошкове и чини код предвидљивијим по времену извршавања.
// Стари приступ
DispatchQueue.main.async {
self.updateUI()
}
// Нови приступ са @MainActor
@MainActor
func updateUI() {
// извршава се на главној нити
self.label.text = "Ажурирано"
}
| Критеријум | @MainActor | DispatchQueue.main |
|---|---|---|
| Провера | компајлер | runtime |
| Синтакса | анотација (декларативно) | позив (императивно) |
| Додатни трошкови | ниски (пребацивање извршиоца) | средњи (замицање + ред) |
| Тестираност | висока (MainActor.shared се може заменити) | ниска (тешко моковати) |
У реалним iOS пројектима @MainActor се примењује у ViewModel слојевима, SwiftUI приказима и UIKit контролерима. Анотација може бити примењена како на појединачне методе, тако и на читав тип у целини.
Обележавањем класе са @MainActor гарантујете да су све њене методе и особине доступне само на главној нити. Ово је посебно погодно за SwiftUI приказе и ObservableObject класе: једноставно додате @MainActor испред class и сва @Published својства се ажурирају сигурно.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
При раду са старим кодом на UIKit-у, где је пребацивање нити било ручно, можете користити MainActor.run за експлицитно пребацивање. Ово је погодно за инкрементални прелазак на Swift Concurrency без преписивања читаве кодовне базе.
await MainActor.run {
self.tableView.reloadData()
}
Упркос свим предностима, @MainActor има низ ограничења која је важно узети у обзир при пројектовању архитектуре апликације. Разумевање граница примене помаже да се избегне некоректна употреба.
Ако је читав ланац позива означен са @MainActor, онда ће се сваки тежак посао извршавати на главној нити, изазивајући замрзавања UI-ја. Препоручује се да се @MainActor-ом означава само UI слој, а да се пословна логика и мрежни захтеви остављају позадинским акторима или глобалном извршиоцу.
Стари API-ји засновани на callback-овима (на пример, URLSession без async/await) не подржавају контекст актора. За интеграцију је потребан омотач са CheckedContinuation. Такође, @MainActor није спојив са performSelector, target-action и другим неасинхроним обрасцима UIKit-а.
При отклањању грешака у апликацијама са @MainActor-ом теже је репродуковати услове трке (race conditions), јер компајлер спречава многе од њих у фази изградње, а не у рантајму. Међутим, то може створити лажни осећај сигурности: некоректно руковање дељеним mutable објектима (на пример, NSCache или заједничке глобалне променљиве) и даље је могуће ако нису означени са @MainActor и користе се без експлицитне синхронизације.
@MainActor значајно поједностављује тестирање UI логике, јер уклања потребу за ручним пребацивањем нити у тестовима. Међутим, постоје карактеристике које треба узети у обзир при писању unit тестова и UI тестова.
У XCTest-у окружење за извршавање аутоматски подешава извршиоца главне нити. Када се тест метод покреће на главној нити, позив @MainActor функција не захтева додатна подешавања — оне се извршавају у истом контексту. За тестирање позадинских сценарија користите MainActor.run унутар Task-а са експлицитним навођењем приоритета и извршиоца, посебно проверавајући да код коректно ради при позиву из позадине.
Један од уобичајених приступа је тестирање ViewModel-а са @MainActor-ом, при чему се проверава да се @Published својства коректно ажурирају након асинхроних операција. Захваљујући наслеђивању контекста актора, позив await унутар теста гарантује извршавање на главној нити без додатних DispatchQueue гаранција и ручног пребацивања контекста, што поједностављује писање тестова.
При рефакторингу постојећег кода на Swift Concurrency проверавајте изолацију @MainActor-а преко компајлера: сви позиви синхроних метода без @MainActor-а из @MainActor контекста означавају се као грешка. Ово својство користи се за постепено превођење пројекта на async/await: означите ViewModel слој са @MainActor и компајлер истиче све несигурне позиве које треба преместити у позадинске акторе.
При креирању мокова за @MainActor зависности користите протоколе са async методима који декларишу асинхроне функције са повратним типовима. То омогућава замену мрежних сервиса, база података и других спољашњих зависности без нарушавања акторске изолације. Компајлер ће проверити да мок испуњава све захтеве изолације, спречавајући случајни приступ @MainActor коду из позадинских тест нити.
При синхроном тестирању @MainActor кода користите XCTestExpectation за чекање завршетка асинхроних операција. Подесите очекивање у тесту и извршавајте fulfillment унутар closure-а који се извршава на главној нити. Ако тест виси бесконачно — највероватније се позив на главној нити не догађа, па је потребно проверити изолацију актора. За отклањање грешака у контексту извршавања корисно је додати проверу Thread.isMainThread унутар тест кода.
Често постављана питања
Не, довољно је означити само методе које ажурирају UI. Међутим, ако у класи постоји неколико таквих метода, једноставније је додати @MainActor читавој класи. То гарантује да се сви њени чланови извршавају на главној нити и поједностављује одржавање кода.
@MainActor је конкретна инстанца глобалног актора везана за главну нит. @globalActor је протокол за стварање сопствених глобалних актора. На пример, можете креирати @BackgroundActor за извршавање кода на позадинској нити, ако то захтева архитектура пројекта.
Да, синхроне функције са @MainActor-ом такође се извршавају на главној нити. Међутим, главна вредност @MainActor-а открива се управо са async/await-ом, када се асинхрона функција аутоматски наставља на главној нити без ручног пребацивања преко DispatchQueue.main.
Task.cancel() ради са @MainActor задацима исто као и са обичним. @MainActor задатак може да проверава Task.isCancelled или да баци CancellationError. При отказивању главна нит се не блокира — задатак једноставно престаје са извршавањем на најближој тачки суспензије.
Компајлер гарантује сигурност: ако позовете @MainActor функцију из позадинског контекста, компајлер ће указати на грешку. За асинхроне позиве довољно је да позивни код означите са await, и извршилац ће се сам пребацити на главну нит. За синхроне позиве потребно је експлицитно пребацивање преко MainActor.run.
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође