@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-небезопасность — обновление интерфейса из фонового потока. Apple встроила @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 и вызывает другую асинхронную функцию, по умолчанию она наследует контекст актора. Это означает, что все вложенные вызовы также выполняются на главном потоке, если не указано иное. Компилятор отслеживает это и выдаёт ошибку при попытке передать несогласованное замыкание.
Сравнение @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-слой, а бизнес-логику и сетевые запросы оставлять в фоновых акторах или глобальном исполнителе.
Старые callback-based API (например, URLSession без async/await) не поддерживают контекст актора. Для интеграции требуется обёртка с CheckedContinuation. Также @MainActor не совместим с performSelector, target-action и другими неасинхронными паттернами UIKit.
При отладке приложений с @MainActor сложнее воспроизвести состояния гонки, так как компилятор предотвращает многие из них на этапе сборки, а не в рантайме. Однако это может создать ложное чувство безопасности: неправильная работа с разделяемыми 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 или Throw на CancellationError. При отмене главный поток не блокируется — задача просто прекращает выполнение на ближайшей точке приостановки.
Компилятор гарантирует безопасность: если вы вызываете @MainActor-функцию из фонового контекста, компилятор укажет на ошибку. Для асинхронных вызовов достаточно пометить вызывающий код await, и исполнитель сам переключится на главный поток. Для синхронных — требуется явное переключение через MainActor.run.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также