Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
MainActor — это новый атрибут из Swift 5.5, который представляет из себя глобальный актор, выполняющий свои задачи в главном потоке (main thread). При создании приложений очень важно следить за тем, чтобы задачи обновления UI выполнялись в главном потоке, что при использовании нескольких фоновых потоков (background threads) иногда может быть затруднительно. Использование атрибута @MainActor
поможет вам гарантировать, что ваш UI всегда будет обновляться в главном потоке.
Если вы не очень хорошо разбираетесь в акторах (Actor) в Swift, я рекомендую прочитать мою статью Акторы в Swift: как их использовать и как предотвращать состояние гонки по данным. Глобальные акторы (Global actors) ведут себя аналогично обычным акторам, и в этой статье я не буду вдаваться в подробности того, как работают обычные акторы.
Что такое MainActor?
MainActor — это глобально уникальный актор, который выполняет свои задачи в главном потоке. Его можно применять к свойствам, методам, инстансам и замыканиям для выполнения задач в главном потоке. Он был представлен в предложении SE-0316 Global Actors как пример глобального актора (наследующего протокол GlobalActor).
Пару слов о глобальных акторах
Глобальные акторы можно рассматривать как синглтоны: существует только по одному инстансу каждого из них. На данный момент глобальные акторы работают только при включении экспериментального параллелизма (experimental concurrency). Вы можете сделать это, добавив следующее значение в «Other Swift Flags» в настройках сборки Xcode:
-Xfrontend -enable-experimental-concurrency
Как только это функция включена, мы можем определить наш собственный глобальный актор следующим образом:
@globalActor
actor SwiftLeeActor {
static let shared = SwiftLeeActor()
}
Свойство shared
является обязательным требованием протокола GlobalActor — оно гарантирует наличие инстанса глобально уникального актора. После определения вы можете использовать глобальный актор в своем проекте так же, как и другие акторы:
@SwiftLeeActor
final class SwiftLeeFetcher {
// ..
}
Как использовать MainActor в Swift?
Глобальный актор может быть применен к свойствам, методам, замыканиям и инстансам. Например, мы могли бы добавить атрибут MainActor к модели представления (view model), чтобы заставить ее выполнять все свои задачи в главном потоке:
@MainActor
final class HomeViewModel {
// ..
}
Используя ключевое слово nonisolated, мы можем быть уверены, что методы без потребности исполнения в главном потоке работают максимально быстро. Класс может быть аннотирован глобальным актором только в том случае, если у него нет суперкласса, суперкласс аннотирован тем же глобальным актором или суперклассом является NSObject. Подкласс класса, аннотированного глобальным актором, должен быть изолирован тем же глобальным актором.
В иных случаях мы могли бы аннотировать с помощью глобального актора отдельные свойства:
final class HomeViewModel {
@MainActor var images: [UIImage] = []
}
Аннотирование свойства images
@MainActor
‘ом гарантирует, что оно может быть изменено только из главного потока:
Этим атрибутом также могут быть аннотированны отдельные методы:
@MainActor func updateViews() {
// Выполняем обновление пользовательского интерфейса ..
}
И даже замыкания могут быть аннотированны на выполнение в главном потоке:
func updateData(completion: @MainActor @escaping () -> ()) {
/// Пример диспатча для имитации поведения
DispatchQueue.global().async {
async {
await completion()
}
}
}
Прямое использование MainActor’а
MainActor в Swift поставляется с расширением для прямого использования:
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
extension MainActor {
/// Выполнение данного замыкания тела на MainActor’е.
public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T
}
Это позволяет нам использовать MainActor непосредственно из методов, даже если мы не определили ни одно его тело с использованием этого атрибута глобального актора:
async {
await MainActor.run {
// Выполняем обновление пользовательского интерфейса
}
}
Другими словами, больше нет реальной необходимости использовать DispatchQueue.main.async
.
Когда следует использовать атрибут MainActor?
До Swift 5.5 вы могли определять множество операторов диспетчеризации, чтобы гарантировать, что задачи выполняются в основном потоке. Например вот так:
func fetchData(completion: @escaping (Result<[UIImage], Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: "..some URL")) { data, response, error in
// .. Декодируем данные в результат
DispatchQueue.main.async {
completion(result)
}
}
}
В приведенном выше примере мы почти уверены, что диспатч необходим. Однако в других случаях диспатч может быть ненужен, поскольку мы уже находимся в главном потоке. Это привело бы к избыточной диспетчеризации, которую можно было бы избежать.
В этих случаях имеет смысл определить свойства, методы, инстансы или замыкания с MainActor, чтобы убедиться, что задачи выполняются в основном потоке. Мы могли бы, например, переписать приведенный выше пример следующим образом:
func fetchData(completion: @MainActor @escaping (Result<[UIImage], Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: "..some URL")!) { data, response, error in
// .. Декодируем данные в результат
let result: Result<[UIImage], Error> = .success([])
async {
await completion(result)
}
}
}
Поскольку сейчас мы работаем с замыканием, определяемым актором, нам необходимо использовать технику async await для вызова нашего замыкания. Использование атрибута @MainActor
позволяет компилятору Swift оптимизировать наш код для повышения производительности.
Выбор правильной стратегии
Используя акторы, важно выбирать правильную стратегию. В приведенном выше примере мы решили сделать замыкание актором, что означает, что кто бы ни использовал наш метод, коллбек по завершению будет выполняться с помощью MainActor. В некоторых случаях это может быть не нужно, если, например, метод, запросивший данные, также вызывается из места, где нет необходимости обрабатывать коллбек завершения в главном потоке.
В таких случаях, вероятно, лучше возложить ответственность диспетчить в нужную очередь на других разработчиков:
viewModel.fetchData { result in
async {
await MainActor.run {
// Обработка результата
}
}
}
Заключение
Глобальные акторы — отличное дополнение к акторам в Swift. Они позволяют нам повторно использовать обычные акторы и дают возможность выполнять задачи пользовательского интерфейса более продуктивно, поскольку компилятор может оптимизировать наш код изнутри. Глобальные акторы могут использоваться для свойств, методов, инстансов и замыканий, после чего компилятор позаботится о соблюдении их требований в нашем коде.
Если вы хотите почитать больше советов по Swift, посетите страницу о Swift. Не стесняйтесь связаться со мной или написать мне в Твиттере, если у вас есть какие-либо дополнительные советы или отзывы.
Материал подготовлен в рамках курса «iOS Developer. Professional».
Всех желающих приглашаем на двухдневный онлайн-интенсив «Пишем современное iOS приложение на SwiftUI». В первый день разберем особенности создания UI с помощью данного фреймворка. Во второй — напишем бизнес-логику с помощью нативных средств (Combine). Также будем использовать новинки, представленные на WWDC 2021, в том числе и async-await.РЕГИСТРАЦИЯ