Как использовать @MainActor для запуска кода в основной очереди

Обновлено для Xcode 13.2

@MainActor — это глобальный актор, который использует основную очередь для выполнения своей работы. На практике это означает, что методы или типы, помеченные @MainActor, могут (по большей части) безопасно изменять пользовательский интерфейс, поскольку он всегда будет выполняться в основной очереди, а вызов MainActor.run() приведет к выполнению некоторых пользовательских действий по вашему выбору на MainActor и, таким образом, в основную очередь. На простейшем уровне обе эти функции просты в использовании, но, как вы увидите, за ними стоит большая сложность.

Во-первых, давайте рассмотрим использование @MainActor, которое автоматически запускает один или все методы типа для основного актора. Это особенно полезно для любых существующих типов для обновления пользовательского интерфейса, таких как классы ObservableObject.

Например, мы могли бы создать наблюдаемый объект с двумя свойствами @Published, и, поскольку они оба будут обновлять пользовательский интерфейс, мы пометим весь класс @MainActor, чтобы гарантировать, что эти обновления пользовательского интерфейса всегда происходят на MainActor:

@MainActor
class AccountViewModel: ObservableObject {
    @Published var username = "Anonymous"
    @Published var isAuthenticated = false
}

На самом деле, эта настройка настолько важна для работы ObservableObject, что SwiftUI запекает ее прямо в нем: всякий раз, когда вы используете @StateObject или @ObservedObject внутри представления, Swift гарантирует, что все представление работает на Main Actor, чтобы вы могли: Не пытаться случайно публиковать обновления пользовательского интерфейса опасным способом. Еще лучше то, что независимо от того, какие оболочки свойств вы используете, свойство body ваших представлений SwiftUI всегда выполняется на Main Actor.

Означает ли это, что вам не нужно явно добавлять @MainActor к наблюдаемым объектам? Что ж, нет — использование @MainActor с этими классами по-прежнему имеет преимущества, не в последнюю очередь, если они используют async/await для выполнения своей собственной асинхронной работы, такой как загрузка данных с сервера.

Итак, моя рекомендация проста: даже несмотря на то, что SwiftUI гарантирует принадлежность к основному действующему лицу при использовании @ObservableObject, @StateObject и в SwiftUI тела view, хорошей идеей будет добавить атрибут @MainActor ко всем наблюдаемым классам объектов, чтобы быть абсолютно уверенным что все обновления пользовательского интерфейса происходят на главном актере. Если вам нужны определенные методы или вычисляемые свойства, чтобы отказаться от выполнения на основном действующем субъекте, используйте неизолированные методы, как если бы вы использовали обычный субъект.

Важно: я говорил это ранее, но стоит повторить: вы не должны пытаться использовать акторы для ваших наблюдаемых объектов, потому что они должны выполнять свои обновления пользовательского интерфейса на основном акторе, а не на пользовательском актере.

В более широком смысле, любой тип, который имеет объекты @MainActor в качестве свойств, также неявно будет @MainActor с использованием глобального вывода актора — набора правил, которые Swift применяет, чтобы убедиться, что глобальный актор работает, не слишком мешая. Я расскажу об этих правилах в следующей главе, поскольку они достаточно точны.

Магия @MainActor заключается в том, что он автоматически заставляет методы или целые типы выполняться на Main Actor, большую часть времени без какой-либо дополнительной работы с нашей стороны. Раньше нам приходилось делать это вручную, не забывая использовать такой код, как DispatchQueue.main.async() или аналогичный везде, где это было необходимо, но теперь компилятор делает это за нас автоматически.

Будьте осторожны: @MainActor действительно полезен для запуска кода на главном актере, но он не является надежным. Например, если у вас есть класс @MainActor, то теоретически все его методы будут выполняться на основном действующем лице, но один из этих методов может инициировать выполнение кода в фоновой задаче. Например, если вы используете Face ID и вызываете AssessmentPolicy() для аутентификации пользователя, обработчик завершения будет вызываться в фоновом потоке, даже если этот код все еще находится в классе @MainActor.

Если вам нужно спонтанно запустить какой-то код на главном актере, вы можете сделать это, вызвав MainActor.run() и предоставив свою работу. Это позволяет вам безопасно передавать работу главному актеру независимо от того, где в данный момент выполняется ваш код, например:

func couldBeAnywhere() async {
    await MainActor.run {
        print("This is on the main actor.")
    }
}

await couldBeAnywhere()

Вы можете ничего не возвращать из run(), если хотите, или возвращать значение, подобное этому:

func couldBeAnywhere() async {
    let result = await MainActor.run { () -> Int in
        print("This is on the main actor.")
        return 42
    }

    print(result)
}

await couldBeAnywhere()

Еще лучше, если этот код уже был запущен на главном действующем объекте, тогда код выполняется немедленно — он не будет ждать следующего цикла выполнения, как это сделал бы DispatchQueue.main.async().

Если вы хотите, чтобы работа была отправлена главному актору, не дожидаясь ее результата, вы можете поместить ее в новую задачу следующим образом:

func couldBeAnywhere() {
    Task {
        await MainActor.run {
            print("This is on the main actor.")
        }
    }

    // more work you want to do
}

couldBeAnywhere()

Или вы также можете пометить замыкание своей задачи как @MainActor, например:

func couldBeAnywhere() {
    Task { @MainActor in
        print("This is on the main actor.")
    }

    // more work you want to do
}

couldBeAnywhere()

Это особенно полезно, когда вы находитесь внутри синхронного контекста, поэтому вам нужно передать работу главному действующему лицу без использования ключевого слова await.

Важно: если ваша функция уже запущена на Main Actor, использование await MainActor.run() запустит ваш код немедленно, не дожидаясь следующего цикла выполнения, но использование Task, как показано выше, будет ждать следующего цикла выполнения.

Вы можете увидеть это в действии в следующем фрагменте:

@MainActor class ViewModel: ObservableObject {
    func runTest() async {
        print("1")

        await MainActor.run {
            print("2")

            Task { @MainActor in
                print("3")
            }

            print("4")
        }

        print("5")
    }
}

Это помечает весь тип как использующий главное действующее лицо, поэтому вызов MainActor.run() будет выполняться немедленно при вызове runTest(). Однако внутренняя задача не запустится сразу, поэтому код выведет 1, 2, 4, 5, 3.

Хотя можно создать своих собственных глобальных акторов, я думаю, что нам, вероятно, следует избегать этого, пока у нас не будет достаточно возможности создавать приложения, используя то, что у нас уже есть.