В каком контексте можно вызывать async/await методы в Swift?

Ответ

Методы, помеченные async, можно вызывать только из асинхронного контекста с использованием ключевого слова await. Основные способы создания такого контекста:

1. Внутри другой async функции:

func fetchUserData() async throws -> User {
    // Вызов асинхронного метода с await
    let data = try await networkService.fetchData(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

2. В блоке Task: Это основной способ запуска асинхронного кода из синхронного контекста (например, из метода viewDidLoad).

// Запуск асинхронной задачи
Task {
    // Это асинхронный контекст
    let user = await fetchUserData()
    // Обновление UI должно быть на MainActor
    await MainActor.run {
        self.usernameLabel.text = user.name
    }
}

3. В модификаторе .task в SwiftUI: SwiftUI предоставляет специальный модификатор для безопасного запуска асинхронных задач, связанных с жизненным циклом View.

struct ContentView: View {
    @State private var data: String = ""

    var body: some View {
        Text(data)
            .task { // Асинхронный контекст, созданный SwiftUI
                data = await loadData()
            }
    }
}

Важные правила:

  • await не блокирует поток: Он приостанавливает выполнение текущей async функции, позволяя потоку выполнять другую работу, пока ожидается результат.
  • Вызов из синхронного контекста запрещен: Нельзя использовать await просто так. Нужно обернуть вызов в Task или находиться внутри async функции.
  • Обработка ошибок: Асинхронные функции, которые могут выбросить ошибку (throws), требуют использования try await.

Ответ 18+ 🔞

О, смотри-ка, какой вопрос подъехал! Про эти ваши async да await. Ну что ж, давай разжую, как есть, чтобы даже у самого тупого мартышлюшки в голове щёлкнуло.

Вот представь, что async метод — это как сосед, который вечно одалживает у тебя дрель, но вернёт её только завтра. Ты не можешь просто так взять и крикнуть ему из окна: «Вася, дрель!» и тут же её получить. Нет, блядь. Ты должен либо подождать (это наш await), либо заняться чем-то другим, пока он её не принесёт.

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

1. Внутри другой такой же async конуры. Это как если ты сам такой же Вася, который кому-то свою болгарку обещал. Ты звонишь другому челу и говоришь: «Мужик, я тебе болгарку отдам, как только мне сосед дрель вернёт». И ждёшь. В коде это выглядит так:

func fetchUserData() async throws -> User {
    // Тут можно ждать, потому что мы уже в async-функции
    let data = try await networkService.fetchData(from: url) // Ждём, пока сеть не выдаст данные
    return try JSONDecoder().decode(User.self, from: data)
}

2. В блоке Task. А это что? Это твой личный раб, которого ты нанимаешь специально, чтобы он сходил к Васе за дрелью, пока ты на диване валяешься. Ты из своего обычного, синхронного мира (типа viewDidLoad) говоришь: «Эй, раб, иди сделай асинхронную хуйню».

// Ты в синхронном методе, просто так ждать нельзя.
// Но можно создать Task — это как отдельную пещеру для асинхронщины.
Task {
    // А вот тут внутри уже можно! Это отдельная асинхронная берлога.
    let user = await fetchUserData() // Ждём пользователя
    // UI трогать надо на главном потоке, это святое
    await MainActor.run {
        self.usernameLabel.text = user.name // Обновляем интерфейс
    }
}
// И пошёл дальше, не блокируя главный поток. Красота!

3. В модификаторе .task в SwiftUI. SwiftUI — он умный, он сам знает, когда view появляется и исчезает. И даёт тебе специальную коробочку .task, куда можно положить асинхронный код. Удалили view — коробочку выкинули, задача отменилась. Удобно, ёпта!

struct ContentView: View {
    @State private var data: String = ""

    var body: some View {
        Text(data)
            .task { // Эта штука создаёт асинхронный контекст за тебя!
                // И тут опять можно ждать
                data = await loadData() // Ждём загрузки
            }
    }
}

А теперь, блядь, правила, которые надо выжечь на жопе:

  • await — это не sleep. Он не блокирует поток. Он говорит: «Я тут приостановлюсь, отпускаю поток погулять, пусть другие задачи поработают. Как только моя дрель (данные) будет готова — я проснусь и продолжу». Это овердохуища важное отличие от старого подхода.
  • Вызвать await просто так с улицы — нельзя. Это как зайти в чужую квартиру и начать ждать, пока тебе приготовят пельмени. Тебя вышвырнут в шею с ошибкой компиляции. Нужно быть либо в другой async функции, либо в Task, либо в .task.
  • Ошибки. Если твой async метод ещё и throws (может выбросить ошибку), то готовься к двойному протоколу: try await. Сначала «попробуй» (try), потом «подожди» (await). Пиздец, да? Но логично.

Короче, суть в том, чтобы не пытаться ждать там, где система тебе этого не разрешает. Создавай правильный контекст — и будет тебе счастье. Всё остальное — от лукавого и ведёт к крашам и адовым багам.