Vladimir Buikliskii

Vladimir Buikliskii 

iOS разработчик, автор ТГ канала @swiftynew

91subscribers

27posts

goals4
14 of 30 paid subscribers
Наберу 30 подписчиков и начну разбирать алгоритмы
1 of 3
$4.06 of $406 raised
Сбор на верстку и выпуск книги "100 вопросов для iOS-разработчика". Средства пойдут на профессиональную верстку книги и ее последующий выпуск.

Использование MainActor в Swift для выполнения задач в главном потоке

https://www.avanderlee.com/swift/mainactor-dispatch-main-thread/
MainActor — это новый атрибут, представленный в Swift 5.5 в качестве глобального актора, который предоставляет исполнитель (executor), выполняющий свои задачи в главном потоке. При создании приложений крайне важно выполнять операции, связанные с обновлением пользовательского интерфейса (UI), в главном потоке, что может быть проблематично при работе с несколькими фоновыми потоками. Использование атрибута @MainActor поможет убедиться, что ваш UI всегда обновляется в главном потоке.
Если вы новичок в теме акторов (Actors) в Swift, рекомендую ознакомиться с моей статьёй «Actors in Swift: how to use and prevent data races». Глобальные акторы работают похожим образом, поэтому в этой статье мы не будем углубляться в то, как устроены акторы в целом.
Что такое MainActor?
MainActor — это глобально уникальный актор, который выполняет свои задачи в главном потоке. Вы можете использовать его для свойств, методов, экземпляров и замыканий, чтобы гарантировать выполнение задач именно в главном потоке. Предложение SE-0316 Global Actors представило main actor в качестве примера глобального актора, который наследуется от протокола GlobalActor.
Понимание глобальных акторов (Global Actors)
Прежде чем мы перейдём к тому, как использовать MainActor в вашем коде, важно понять концепцию глобальных акторов. Их можно рассматривать как синглтоны: существует ровно один такой экземпляр. Мы можем определить глобальный актор следующим образом:
@globalActor
actor SwiftLeeActor {
static let shared = SwiftLeeActor()
}
Свойство shared — это требование протокола GlobalActor, гарантирующее наличие глобально уникального экземпляра актера. После определения вы можете использовать данный глобальный актор во всём проекте точно так же, как и любой другой актор:
@SwiftLeeActor
final class SwiftLeeFetcher {
// ..
}
Везде, где вы используете атрибут глобального актора, синхронизация будет происходить через его общий экземпляр (shared), чтобы обеспечить взаимное исключение при доступе к объявлениям (например, свойствам и методам). Результат аналогичен работе акторов в целом, как описано в статье «Actors in Swift: how to use and prevent data races».
Внутренняя реализация @MainActor
Реализация @MainActor во многом похожа на наш пользовательский актор @SwiftLeeActor:
@globalActor
final actor MainActor: GlobalActor {
static let shared: MainActor
}
Этот актор доступен по умолчанию и определён внутри фреймворка для параллелизма (concurrency). Иными словами, вы можете сразу же начать использовать этот глобальный актор и помечать свой код для выполнения в главном потоке, синхронизируясь посредством данного глобального актора.
Как использовать MainActor в Swift?
Вы можете применять глобальный актор (global actor) к свойствам, методам, замыканиям и экземплярам. Например, мы можем добавить атрибут @MainActor к view model, чтобы все задачи выполнялись в главном потоке:
@MainActor
final class HomeViewModel {
// ..
}
Используя nonisolated, мы даём возможность методам, которые не требуют выполнения в главном потоке, работать максимально быстро, не дожидаясь освобождения главного потока. Вы можете объявлять класс с глобальным актором только в том случае, если у него нет суперкласса, либо суперкласс также аннотирован тем же глобальным актором, либо же является NSObject. Подкласс класса, аннотированного глобальным актором, обязан быть изолирован к тому же глобальному актору.
В некоторых случаях нам может понадобиться добавить глобальный актор только к отдельным свойствам:
final class HomeViewModel {
@MainActor var images: [UIImage] = []
}
Добавление атрибута @MainActor к свойству images гарантирует, что это свойство может изменяться только из главного потока:
Требования атрибута MainActor контролируются компилятором.Компилятор обеспечивает соблюдение требований к атрибуту MainActor.
Это особенно удобно при работе с архитектурой MVVM в SwiftUI, так как вы хотите, чтобы перерисовка интерфейса происходила только в главном потоке.
Также вы можете аннотировать отдельные методы:
@MainActor func updateViews() {
/// Этот метод всегда будет выполняться в главном потоке,
/// если его вызов происходит в контексте асинхронности.
}
Будьте внимательны: гарантируется, что этот метод будет выполнен в главном потоке только если он вызывается из асинхронного контекста. Начиная с Xcode 16, вы получите соответствующее предупреждение, но важно знать об этой особенности, чтобы правильно понимать, как работает атрибут main actor.
Наконец, вы можете маркировать замыкания для их выполнения в главном потоке:
func updateData(completion: @MainActor @escaping () -> ()) {
Task {
await someHeavyBackgroundOperation()
await completion()
}
}
Хотя в таком случае лучше переписать метод updateData как асинхронный (async), чтобы не передавать завершение через замыкание.
Прямое использование MainActor
В Swift MainActor обладает специальным расширением, позволяющим напрямую использовать актор:
extension MainActor {
/// Выполняет переданное замыкание `body` на главном акторе.
public static func run<T>(resultType: T.Type = T.self,
       body: @MainActor @Sendable () throws -> T) async rethrows -> T
Благодаря этому мы можем обращаться к @MainActor в методах без явного использования атрибута внутри тела метода:
Task {
await someHeavyBackgroundOperation()
await MainActor.run {
// Выполняем обновление UI
}
}
Другими словами, больше нет необходимости использовать DispatchQueue.main.async. Однако я рекомендую применять глобальный атрибут (global actor), чтобы ограничить доступ к главному потоку. Без аннотации глобального актора любой разработчик может забыть вызвать MainActor.run, что может привести к обновлениям UI в фоновом потоке.
Когда следует использовать атрибут MainActor?
До Swift 5.5 вам, скорее всего, приходилось многократно вызывать методы диспатча, чтобы убедиться, что задачи выполняются в главном потоке. Например:
func fetchImage(for url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data, let image = UIImage(data: data) else {
DispatchQueue.main.async {
completion(.failure(ImageFetchingError.imageDecodingFailed))
}
return
}
DispatchQueue.main.async {
    completion(.success(image))
    }
}
.resume()
}
В этом примере мы явно выполняем переключение на главный поток при возврате изображения. Приходится делать это в нескольких местах, что усложняет код и порождает множество замыканий.
Иногда даже может получиться так, что мы вызываем DispatchQueue.main.async, находясь уже в главном потоке, и получаем лишний диспатч, которого можно было избежать. Переписывание кода с использованием async/await и MainActor позволяет оптимизировать вызовы, осуществляя диспатч только при необходимости.
Во всех подобных случаях изоляция свойств, методов, экземпляров или замыканий к MainActor гарантирует, что задачи выполняются в главном потоке. Идеально переписать приведённый выше пример вот так:
@MainActor
func fetchImage(for url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageFetchingError.imageDecodingFailed
    }
    return image
}
Атрибут @MainActor обеспечивает выполнение логики в главном потоке, при этом сетевой запрос по-прежнему выполняется в фоновом режиме. Вызов главного актора (dispatch к MainActor) происходит только тогда, когда это действительно нужно, что положительно сказывается на производительности.
Почему @MainActor не всегда гарантирует выполнение в главном потоке
Если вы только начинаете работать с параллелизмом в Swift, может показаться, что пометка метода аннотацией @MainActor всегда гарантирует его выполнение в главном потоке. Однако это не так для синхронных методов в не изолированном (non-isolated) контексте. Некоторые команды столкнулись с этим на практике, как и Кирилл Чермак (Cyril Cermak), который поделился своим опытом со мной во время одной из конференций.
Прежде чем вдаваться в подробности, важно знать, что режим языка Swift 6 обнаруживает большую часть подобных ситуаций. Но поскольку многие команды всё ещё переходят на Swift 6, я хочу обратить внимание на это распространённое заблуждение.
Синхронные методы в не изолированных контекстах выполняются в том же потоке, что и вызывающий код, независимо от любых аннотаций актора. В зависимости от того, какой режим языка Swift вы используете, компилятор может (или не может) предупредить вас о возможных проблемах.
Например, следующий код без проблем компилируется в режиме языка Swift 5:
Вызов через глобальную очередь (global queue) порождает не изолированный контекст, и атрибут @MainActorв данном случае не учитывается.
Однако при включённом режиме языка Swift 6 компилятор сразу сообщит о потенциальной ошибке:
В больших проектах часто бывает, что цепочка методов вызывается из фонового потока. В итоге такая цепочка может завершиться вызовом метода, помеченного @MainActor, при этом синхронно выполняясь в том же потоке, откуда был сделан вызов, — то есть в фоновом. Поэтому, если возможно, переведите свой код на Swift 6. Если же нет — будьте внимательны при использовании аннотации @MainActor.
Если вы всегда вызываете синхронный метод из кода, который уже работает в главном потоке, проблем может не возникнуть. Однако важно понимать эту тонкость, так как ваши ожидания могут не совпадать с реальностью, и это способно привести к неожиданным сбоям, связанным с UI, до полной миграции на Swift 6.
Subscription levels3

Базовый уровень

$1.34 per month
Подписавшись на базовый уровень, вы получите:
1) Доступ к переводу популярных статей на тему iOS-разработки.

Стандартный уровень

$5.3 per month
Подписавшись на стандартный уровень, вы получите:
1) Доступ к переводу официальной документации Swift(Notion).
2) Доступ к переводу популярных статей на тему iOS-разработки.
3) Подробный список библиотек с инструкциями по их настройке для удобной работы с проектами(Notion -  https://www.notion.so/).
4) Вопросы и ответы из реальных собеседований.
5) Ранний доступ к онлайн-версии книги "100 вопросов для iOS-разработчика" до официального релиза.(Сечйас  доступен черновик)
6) Доступ к закрытому чату.

Продвинутый уровень(Менторство)

$68 per month
Подписавшись на продвинутый уровень, вы получите:
1. Все материалы и преимущества стандартного уровня.
2.  4 часа личных консультаций в месяц (онлайн-созвон или разбор кода/проектов).
Go up