@MainActor — це глобальний актор у мові Swift, який гарантує виконання коду на головному потоці. За даними Apple Developer, 2024, @MainActor автоматизує перемикання на головний потік під час роботи з UI, звільняючи розробника від ручного виклику DispatchQueue.main.async. Анотація з'явилася в Swift 5.5 разом із системою async/await.
Головне
@MainActor — це глобальний актор у 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 або кинути CancellationError. При скасуванні головний потік не блокується — завдання просто припиняє виконання на найближчій точці призупинення.
Компілятор гарантує безпеку: якщо ви викликаєте @MainActor-функцію з фонового контексту, компілятор вкаже на помилку. Для асинхронних викликів достатньо позначити код, що викликає, await, і виконавець сам перемкнеться на головний потік. Для синхронних — потрібне явне перемикання через MainActor.run.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також