Асинхронное программирование в Swift: полное руководство для iOS/macOS

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

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

Начать курс

Введение в асинхронное программирование в Swift

Современные приложения для iOS и macOS требуют высокой отзывчивости и производительности. Асинхронное программирование в Swift позволяет выполнять длительные операции (сетевые запросы, работа с файлами, обработка данных) без блокировки основного потока (Main Thread). Начиная с версии Swift 5.5, Apple представила мощную модель Swift Concurrency, основанную на ключевых словах async и await, которая значительно упрощает написание асинхронного кода по сравнению с традиционными замыканиями (completion handlers) или DispatchQueue.

В этом руководстве мы подробно разберем все ключевые концепции: от основ async/await до продвинутых тем, таких как Actors и Task Groups. Вы узнаете, как писать безопасный, читаемый и эффективный асинхронный код для ваших проектов на Swift.

Основы async/await в Swift

Ключевые слова async и await являются сердцем Swift Concurrency. Они позволяют писать асинхронный код, который выглядит как синхронный, что значительно улучшает читаемость и уменьшает количество ошибок.

Объявление асинхронной функции

Чтобы объявить функцию асинхронной, добавьте ключевое слово async после списка параметров, но перед возвращаемым типом. Такая функция может приостанавливать свое выполнение.

func fetchUserData(from url: URL) async throws -> Data {
    let (data, response) = try await URLSession.shared.data(from: url)
    // Проверка HTTP статуса
    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    return data
}

Вызов асинхронной функции с await

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

func loadProfile() async {
    let url = URL(string: "https://api.example.com/user/profile")!
    do {
        let data = try await fetchUserData(from: url)
        // Обработка данных (например, декодирование JSON)
        print("Данные получены: \(data.count) байт")
    } catch {
        print("Ошибка загрузки профиля: \(error.localizedDescription)")
    }
}

Async let для параллельных задач

Если вам нужно выполнить несколько независимых асинхронных операций одновременно, используйте конструкцию async let. Это позволяет запустить задачи параллельно и дождаться всех результатов.

func loadDashboardData() async -> Dashboard {
    async let user = fetchUserData(from: URL(string: "https://api.example.com/user")!)
    async let posts = fetchUserData(from: URL(string: "https://api.example.com/posts")!)
    async let notifications = fetchUserData(from: URL(string: "https://api.example.com/notifications")!)

    // Дожидаемся всех результатов
    let (userData, postsData, notifData) = await (try user, try posts, try notifications)
    return Dashboard(user: userData, posts: postsData, notifications: notifData)
}

Task и Structured Concurrency

Swift Concurrency основана на концепции Structured Concurrency. Это означает, что каждая асинхронная задача имеет четкое время жизни и родительскую связь. Основным строительным блоком является Task.

Создание новой задачи

Task создает новую асинхронную единицу работы. Вы можете создать задачу в синхронном контексте (например, в viewDidLoad).

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Создаем новую задачу для выполнения асинхронной работы
        Task {
            let data = try await fetchUserData(from: URL(string: "https://api.example.com")!)
            // Обновляем UI (обязательно на главном потоке)
            await MainActor.run {
                self.updateUI(with: data)
            }
        }
    }

    func updateUI(with data: Data) {
        // Обновление интерфейса
    }
}

Task Group для динамического количества задач

Когда количество асинхронных задач неизвестно заранее или меняется динамически, используйте withTaskGroup или withThrowingTaskGroup.

func downloadImages(from urls: [URL]) async throws -> [UIImage] {
    try await withThrowingTaskGroup(of: UIImage.self) { group in
        var images: [UIImage] = []
        images.reserveCapacity(urls.count)

        for url in urls {
            group.addTask {
                let data = try await URLSession.shared.data(from: url).0
                guard let image = UIImage(data: data) else {
                    throw DownloadError.invalidImageData
                }
                return image
            }
        }

        // Собираем результаты по мере их завершения
        for try await image in group {
            images.append(image)
        }
        return images
    }
}

enum DownloadError: Error {
    case invalidImageData
}

Actors: безопасная работа с изменяемым состоянием

Одна из главных проблем многопоточности — гонка данных (data races). Swift предлагает Actors для изоляции изменяемого состояния и защиты от одновременного доступа из разных задач.

Определение Actor

Actor — это ссылочный тип, который защищает свое состояние. Все методы и свойства actor являются isolated по умолчанию. Доступ к ним извне возможен только через await.

actor BankAccount {
    private var balance: Double = 0.0

    func deposit(amount: Double) {
        balance += amount
    }

    func withdraw(amount: Double) throws -> Double {
        guard balance >= amount else {
            throw BankError.insufficientFunds
        }
        balance -= amount
        return amount
    }

    func getBalance() -> Double {
        return balance
    }
}

enum BankError: Error {
    case insufficientFunds
}

Использование Actor

func performTransactions() async {
    let account = BankAccount()

    // Параллельные транзакции безопасны благодаря actor
    async let deposit1 = account.deposit(amount: 100)
    async let deposit2 = account.deposit(amount: 50)
    async let withdrawal = try account.withdraw(amount: 30)

    await deposit1
    await deposit2
    let withdrawn = try await withdrawal

    let finalBalance = await account.getBalance()
    print("Снято: \(withdrawn), Баланс: \(finalBalance)")
}

Продвинутые техники: Continuation и MainActor

Иногда вам нужно интегрировать старый код, основанный на замыканиях или делегатах, с новой моделью async

Блоги

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