@MainActor — що це, застосування та особливості в асинхронному коді Swift

Автор: IT Sectr Опубліковано: 2026-03-19 Час читання: 8 хв

@MainActor — це глобальний актор у мові Swift, який гарантує виконання коду на головному потоці. За даними Apple Developer, 2024, @MainActor автоматизує перемикання на головний потік під час роботи з UI, звільняючи розробника від ручного виклику DispatchQueue.main.async. Анотація з'явилася в Swift 5.5 разом із системою async/await.

Головне

  • @MainActor — глобальний актор Swift для гарантії виконання на головному потоці.
  • Система async/await — основа, на якій побудовано роботу @MainActor.
  • Анотація класу автоматично поміщає всі його методи на головний потік.
  • На відміну від DispatchQueue.main, @MainActor перевіряє потік на рівні компілятора.
  • UI-оновлення — основна сфера застосування @MainActor у iOS-розробці.

Що таке @MainActor?

@MainActor — це глобальний актор у Swift, який поєднує властивості акторів із гарантією виконання на головному потоці застосунку. Він є частиною системи паралелізму Swift, представленої в Swift 5.5 разом з async/await і структурованою конкурентністю. Анотація дозволяє розробнику не думати про ручне перемикання потоків і знижує кількість помилок UI.

Визначення та місце в Swift Concurrency

Актор у Swift — це посилальний тип, який ізолює свій стан і гарантує, що лише один потік може його змінювати. @MainActor — спеціальний глобальний актор, виконавцем якого виступає головний потік. Будь-який код, позначений @MainActor, виконується на головному потоці — навіть якщо він викликаний із фонового завдання.

До появи @MainActor розробники вручну перемикалися на головний потік через DispatchQueue.main.async. Це було джерелом частих помилок: розробники забували перемикатися, що призводило до крашів через оновлення UI не в головному потоці. @MainActor вирішує цю проблему на рівні системи типів.

Передумови створення

Джерелом більшості багів у iOS-застосунках є UI-небезпечність — оновлення інтерфейсу з фонового потоку. Apple вбудувала @MainActor у Swift Concurrency, щоб зробити перемикання на головний потік автоматичним і перевірюваним компілятором, усунувши цілий клас runtime-помилок.

Як працює @MainActor?

Принцип роботи @MainActor базується на системі виконання Swift Concurrency. Коли потік викликає функцію, позначену @MainActor, планувальник призупиняє її на поточному виконавці та відновлює на головному потоці. Компілятор відстежує межі виклику та гарантує безпеку.

Виконавець головного потоку

За виконання @MainActor відповідає MainActor.shared — виконавець, асоційований з головним потоком застосунку. Коли асинхронна функція позначена @MainActor, вона завжди відновлюється на цьому виконавці, незалежно від того, на якому потоці було запущено початкове завдання.

swift
import SwiftUI

class ViewModel: ObservableObject {
    @Published var items: [String] = []

    @MainActor
    func loadData() async {
        let result = await fetchRemoteData()
        items = result  // безпечно, MainActor гарантує головний потік
    }
}

Успадкування контексту актора

Якщо функція позначена @MainActor і викликає іншу асинхронну функцію, вона за замовчуванням успадковує контекст актора. Це означає, що всі вкладені виклики також виконуються на головному потоці, якщо не вказано інше. Компілятор відстежує це та видає помилку при спробі передати неузгоджене замикання.

@MainActor vs DispatchQueue.main

Порівняння @MainActor і DispatchQueue.main допомагає зрозуміти, чому новий механізм вважається безпечнішим і зручнішим, хоча обидва вирішують одне завдання — виконання коду на головному потоці.

Безпека на рівні типів

@MainActor — це перевірка на рівні компілятора. Якщо ви намагаєтеся викликати @MainActor-функцію з небезпечного контексту, компілятор видасть попередження або помилку. DispatchQueue.main.async — це runtime-виклик: код скомпілюється, але може впасти в рантаймі при спробі оновити UI з фонового потоку.

Продуктивність і надмірність

DispatchQueue.main.async додає в чергу блок, який може бути виконаний із затримкою. @MainActor з async/await виконує пряме перемикання виконавця без створення зайвих замикань. Це знижує накладні витрати та робить код більш передбачуваним за часом виконання.

swift
// Старий підхід
DispatchQueue.main.async {
    self.updateUI()
}

// Новий підхід з @MainActor
@MainActor
func updateUI() {
    // виконується на головному потоці
    self.label.text = "Оновлено"
}
Критерій@MainActorDispatchQueue.main
Перевіркакомпіляторruntime
Синтаксисанотація (декларативний)виклик (імперативний)
Накладні витратинизькі (перемикання виконавця)середні (замикання + черга)
Тестованістьвисока (MainActor.shared можна підмінити)низька (важко мокувати)

Використання @MainActor у iOS-проєктах

У реальних iOS-проєктах @MainActor застосовується у ViewModel-шарах, SwiftUI-в'ю та UIKit-контролерах. Анотація може бути застосована як до окремих методів, так і до всього типу цілком.

Анотація класу або структури

Позначивши клас @MainActor, ви гарантуєте, що всі його методи та властивості доступні тільки на головному потоці. Це особливо зручно для SwiftUI-в'ю та ObservableObject-класів: ви просто додаєте @MainActor перед class, і всі @Published-властивості оновлюються безпечно.

swift
@MainActor
final class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false

    func fetchUsers() async {
        isLoading = true
        users = await api.getUsers()
        isLoading = false
    }
}

Обгортання legacy-коду

При роботі зі старим кодом на UIKit, де перемикання потоків було ручним, можна використовувати MainActor.run для явного перемикання. Це зручно для інкрементального переходу на Swift Concurrency без переписування всієї кодової бази.

swift
await MainActor.run {
    self.tableView.reloadData()
}

Обмеження @MainActor

Незважаючи на всі переваги, @MainActor має ряд обмежень, які важливо враховувати при проєктуванні архітектури застосунку. Розуміння меж застосовності допомагає уникнути некоректного використання.

Продуктивність при інтенсивному використанні

Якщо весь ланцюжок викликів позначено @MainActor, то будь-яка важка робота виконуватиметься на головному потоці, викликаючи зависання UI. Рекомендується позначати @MainActor тільки UI-шар, а бізнес-логіку та мережеві запити залишати у фонових акторах або глобальному виконавці.

Несумісність з деякими API

Старі callback-based API (наприклад, URLSession без async/await) не підтримують контекст актора. Для інтеграції потрібне обгортання з CheckedContinuation. Також @MainActor не сумісний з performSelector, target-action та іншими неасинхронними патернами UIKit.

Багатопотокове налагодження

При налагодженні застосунків з @MainActor складніше відтворити стани гонки, оскільки компілятор запобігає багатьом із них на етапі збірки, а не в рантаймі. Однак це може створити хибне відчуття безпеки: неправильна робота зі спільними mutable-об'єктами (наприклад, NSCache або спільні глобальні змінні) все ще можлива, якщо вони не позначені @MainActor і використовуються без явної синхронізації.

Тестування @MainActor

@MainActor значно спрощує тестування UI-логіки, оскільки позбавляє необхідності вручну перемикати потоки в тестах. Однак існують особливості, які потрібно враховувати при написанні unit-тестів та UI-тестів.

Unit-тести з MainActor

У 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 всередині тестового коду.

Часті запитання

Чи обов'язково позначати весь клас @MainActor?

Ні, достатньо позначити тільки методи, які оновлюють UI. Однак якщо в класі кілька таких методів, простіше додати @MainActor до всього класу. Це гарантує, що всі його члени виконуються на головному потоці, і спрощує підтримку коду.

Чим @MainActor відрізняється від @globalActor?

@MainActor — це конкретний екземпляр глобального актора, прив'язаний до головного потоку. @globalActor — це протокол для створення власних глобальних акторів. Наприклад, ви можете створити @BackgroundActor для виконання коду на фоновому потоці, якщо це потрібно архітектурою проєкту.

Чи можна використовувати @MainActor без async/await?

Так, синхронні функції з @MainActor також виконуються на головному потоці. Однак основна цінність @MainActor розкривається саме з async/await, коли асинхронна функція автоматично відновлюється на головному потоці без ручного перемикання через DispatchQueue.main.

Як скасувати @MainActor-завдання?

Task.cancel() працює з @MainActor-завданнями так само, як і зі звичайними. @MainActor-завдання може перевіряти Task.isCancelled або кинути CancellationError. При скасуванні головний потік не блокується — завдання просто припиняє виконання на найближчій точці призупинення.

Що буде, якщо викликати @MainActor з фонового потоку?

Компілятор гарантує безпеку: якщо ви викликаєте @MainActor-функцію з фонового контексту, компілятор вкаже на помилку. Для асинхронних викликів достатньо позначити код, що викликає, await, і виконавець сам перемкнеться на головний потік. Для синхронних — потрібне явне перемикання через MainActor.run.

Підсумки

  • @MainActor — глобальний актор Swift, що гарантує виконання на головному потоці.
  • Перевірка компілятором усуває цілий клас помилок UI-безпеки.
  • Анотація всього класу автоматично поміщає всі його методи на головний потік.
  • MainActor.run — явне перемикання для legacy-коду та синхронних контекстів.
  • На відміну від DispatchQueue.main, @MainActor не створює замикань і використовує перемикання виконавця.
  • Важкі обчислення не повинні виконуватися під @MainActor, щоб уникнути зависань UI.
  • Успадкування контексту актора спрощує ланцюжки асинхронних викликів і робить код послідовним, передбачуваним та безпечним для UI.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також