Async/Await для Swift: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс
Async/Await Swift: методы, примеры, установка | PythonLib

Async/Await в Swift: полное руководство по структурированной конкурентности

Современная разработка под Apple-экосистему требует эффективной работы с асинхронными операциями: загрузка данных из сети, работа с файлами, обработка изображений. До появления async/await разработчики использовали замыкания (completion handlers) или Combine, что приводило к "callback hell" и сложному управлению состоянием. Swift 5.5 представил встроенную поддержку асинхронных функций, изменив подход к конкурентному программированию.

Async/Await — это не библиотека, а встроенная языковая конструкция Swift, позволяющая писать асинхронный код так же просто, как синхронный. Ключевые слова async и await делают конкурентный код линейным, читаемым и безопасным. Вместо вложенных замыканий вы пишете последовательные вызовы, а компилятор сам управляет переключением контекстов.

Эта статья — полное руководство по async/await в Swift. Вы узнаете, как установить минимальную версию Swift, освоите основные методы (Task, TaskGroup, Actor, async let и другие), увидите реальный пример приложения для загрузки и обработки изображений, а также получите советы по производительности и типичным ошибкам.

1. Что это и зачем нужно?

Async/Await — это модель конкурентности, встроенная в язык Swift начиная с версии 5.5. Она позволяет объявлять функции как async (асинхронные) и приостанавливать их выполнение до получения результата с помощью await. Это решает три ключевые проблемы:

  • Читаемость: код выглядит как синхронный, без вложенных замыканий.
  • Безопасность: компилятор проверяет, что асинхронные функции вызываются только в асинхронном контексте.
  • Производительность: эффективное использование потоков без ручного управления.

Зачем это нужно? Представьте, что вам нужно загрузить три изображения, обработать их и сохранить. Без async/await вы бы писали цепочку замыканий с обработкой ошибок. С async/await это три строки с await и одним do-catch.

2. Установка

Async/Await — это часть языка Swift, поэтому не требует установки дополнительных библиотек. Вам нужен только Xcode 13+ или Swift 5.5+.

Проверка версии Swift

swift --version
# Apple Swift version 5.9 (swiftlang-5.9.0.128.2 clang-1500.0.40.1)
# Target: arm64-apple-macosx14.0

Минимальные требования

  • Xcode: 13.0+ (рекомендуется 15.0+)
  • Swift: 5.5+ (рекомендуется 5.9+)
  • Платформы: iOS 13+, macOS 10.15+, watchOS 6+, tvOS 13+
Важно: Некоторые возможности (например, AsyncSequence) требуют iOS 15+ / macOS 12+.

Создание нового проекта

# Swift Package Manager
mkdir AsyncDemo
cd AsyncDemo
swift package init --type executable
# Откройте Package.swift и укажите swift-tools-version: 5.7+

Или просто создайте новый playground в Xcode и выберите шаблон "iOS" или "macOS".

3. Быстрый старт — минимальный рабочий пример

Напишем простую программу, которая загружает данные из сети и выводит их длину.

import Foundation

// Асинхронная функция для загрузки данных
func fetchData(from urlString: String) async throws -> Data {
    guard let url = URL(string: urlString) else {
        throw URLError(.badURL)
    }
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

// Основная асинхронная точка входа
@main
struct AsyncDemoApp {
    static func main() async {
        do {
            let data = try await fetchData(from: "https://jsonplaceholder.typicode.com/posts/1")
            print("Загружено \(data.count) байт")
            if let text = String(data: data, encoding: .utf8) {
                print("Содержимое: \(text.prefix(100))...")
            }
        } catch {
            print("Ошибка: \(error.localizedDescription)")
        }
    }
}

Как это работает:

  1. Функция fetchData помечена как async throws — она может приостанавливаться и выбрасывать ошибки.
  2. await URLSession.shared.data(from: url) приостанавливает выполнение до завершения сетевого запроса.
  3. Точка входа main() async позволяет запускать асинхронный код из командной строки.

4. Основные методы, функции и классы

Рассмотрим 10 ключевых элементов async/await в Swift. Каждый включает сигнатуру, описание и пример.

4.1. async — объявление асинхронной функции

Сигнатура: func имя(параметры) async -> Тип

Что делает: Помечает функцию как асинхронную. Такая функция может приостанавливать своё выполнение с помощью await.

func calculatePiAsync(iterations: Int) async -> Double {
    var pi: Double = 0
    for k in 0..<iterations {
        pi += Double((k % 2 == 0 ? 1 : -1)) / Double(2 * k + 1)
        // Искусственная приостановка для демонстрации
        if k % 100000 == 0 {
            await Task.yield()
        }
    }
    return pi * 4
}

// Использование
let pi = await calculatePiAsync(iterations: 1000000)
print("Pi ≈ \(pi)")

4.2. await — ожидание асинхронного результата

Сигнатура: let результат = await асинхронныйВызов()

Что делает: Приостанавливает текущую задачу до завершения асинхронной операции, не блокируя поток.

func downloadAndProcess() async {
    let data = await fetchData(from: "https://example.com/data.json")
    let processed = await processData(data)
    print("Результат: \(processed)")
}

// Примечание: await можно использовать только внутри async-контекста

4.3. Task — создание новой асинхронной задачи

Сигнатура: Task(priority: TaskPriority?, operation: () async -> Success)

Что делает: Создаёт новую асинхронную задачу, выполняющуюся в параллельном контексте. Возвращает Task, который можно отменить или дождаться.

func runTasks() async {
    // Задача с высоким приоритетом
    let task1 = Task(priority: .high) {
        return await fetchData(from: "https://api.example.com/urgent")
    }
    
    // Задача с фоновым приоритетом
    let task2 = Task(priority: .background) {
        return await fetchData(from: "https://api.example.com/background")
    }
    
    // Ожидание результатов
    let result1 = await task1.value
    let result2 = await task2.value
    print("Оба результата получены")
}

4.4. TaskGroup — группа параллельных задач

Сигнатура: withTaskGroup(of: ChildTaskResult, returning: GroupResult, body: (inout TaskGroup) async -> GroupResult)

Что делает: Позволяет запускать несколько параллельных задач и собирать их результаты. Все задачи в группе выполняются конкурентно.

func downloadAllImages(urls: [URL]) async throws -> [Data] {
    return try await withTaskGroup(of: Data.self) { group in
        var results: [Data] = []
        
        for url in urls {
            group.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                return data
            }
        }
        
        // Сбор результатов по мере завершения
        for try await data in group {
            results.append(data)
        }
        
        return results
    }
}

4.5. async let — конкурентное связывание

Сигнатура: async let имя = выражение

Что делает: Запускает асинхронную операцию параллельно с текущим контекстом. Результат становится доступен при первом использовании await.

func fetchUserAndPosts() async throws -> (User, [Post]) {
    async let user = fetchUser()
    async let posts = fetchPosts()
    
    // Обе задачи выполняются параллельно
    return try await (user, posts)
}

// Использование
let (user, posts) = try await fetchUserAndPosts()
print("Пользователь: \(user.name), постов: \(posts.count)")

4.6. Actor — потокобезопасный тип

Сигнатура: actor Имя { свойства и методы }

Что делает: Защищает состояние от гонок данных. Все методы actor выполняются последовательно на одном исполнителе.

actor Counter {
    private var value = 0
    
    func increment() {
        value += 1
    }
    
    func getValue() -> Int {
        return value
    }
}

// Использование
let counter = Counter()
await counter.increment()
let current = await counter.getValue()
print("Счётчик: \(current)")

4.7. MainActor — выполнение на главном потоке

Сигнатура: @MainActor func имя() или await MainActor.run { ... }

Что делает: Гарантирует выполнение кода на главном потоке (UI-потоке). Критически важно для обновления интерфейса.

@MainActor
class ViewModel: ObservableObject {
    @Published var text = ""
    
    func updateUI() async {
        // Этот метод гарантированно выполняется на главном потоке
        let data = await fetchData(from: "https://api.example.com")
        text = String(data: data, encoding: .utf8) ?? ""
    }
}

// Альтернативный способ
await MainActor.run {
    self.label.text = "Обновлено"
}

4.8. AsyncSequence — асинхронная последовательность

Сигнатура: protocol AsyncSequence { associatedtype Element; func makeAsyncIterator() -> AsyncIterator }

Что делает: Позволяет итерировать элементы, которые становятся доступны асинхронно (например, строки из файла или события из сети).

import Foundation

// Чтение файла построчно с AsyncSequence
func readLines(from url: URL) -> AsyncLine                    

Рекомендаций