Асинхронное программирование в Swift: полное руководство для начинающих

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

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

Начать курс

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

Современные мобильные и десктопные приложения должны быть отзывчивыми и быстрыми. Никто не любит, когда интерфейс зависает при загрузке данных из сети или обработке большого файла. Именно здесь на помощь приходит асинхронное программирование. В Swift, начиная с версии 5.5, появился мощный и интуитивно понятный инструментарий для работы с асинхронностью — async/await, Task и Actor.

В этом руководстве мы разберем основы асинхронного программирования в Swift для iOS и macOS. Вы узнаете, как писать асинхронный код без коллбэков (callback hell), правильно управлять потоками и избегать состояний гонки. Материал рассчитан на начинающих разработчиков, которые уже знакомы с основами синтаксиса Swift.

Что такое асинхронное программирование и зачем оно нужно?

Представьте, что ваше приложение — это повар на кухне. Если повар будет ждать, пока закипит чайник, и ничего не делать, — обед затянется. Асинхронное программирование позволяет «повару» (главному потоку) поставить чайник и, не дожидаясь его закипания, заняться нарезкой салата. Когда чайник закипит, повар вернется к нему.

В контексте iOS это означает, что тяжелые операции (загрузка изображения, парсинг JSON, работа с базой данных) выполняются в фоне, не блокируя главный поток, который отвечает за обновление интерфейса. Если этого не делать, приложение «зависнет» и вскоре будет принудительно закрыто системой.

Проблема старого подхода: коллбэки и GCD

До появления async/await разработчики использовали Grand Central Dispatch (GCD) и замыкания (коллбэки). Это приводило к так называемому «аду коллбэков» — вложенным блокам кода, которые было сложно читать, отлаживать и поддерживать.

// Старый стиль с GCD и замыканиями
func fetchUserData(completion: @escaping (User?) -> Void) {
    DispatchQueue.global().async {
        // Долгая операция
        let user = User(name: "Иван")
        DispatchQueue.main.async {
            completion(user)
        }
    }
}

fetchUserData { user in
    guard let user = user else { return }
    fetchUserPosts(userId: user.id) { posts in
        // Еще один вложенный коллбэк
        print(posts)
    }
}

Современный Swift предлагает гораздо более элегантное решение.

Основы Swift Concurrency: async/await

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

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

// Асинхронная функция
func fetchImage(from url: URL) async throws -> UIImage {
    let (data, _) = try await URLSession.shared.data(from: url)
    guard let image = UIImage(data: data) else {
        throw URLError(.badServerResponse)
    }
    return image
}

// Вызов функции
Task {
    do {
        let image = try await fetchImage(from: url)
        self.imageView.image = image
    } catch {
        print("Ошибка загрузки: \(error)")
    }
}

Обратите внимание: URLSession.shared.data(from:) — это уже готовая асинхронная функция от Apple. Мы просто вызываем её с await, и поток освобождается для других задач, пока данные загружаются.

Последовательные и параллельные вызовы

Вы можете вызывать асинхронные функции последовательно или параллельно.

// Последовательный вызов: второй запрос ждет первый
let user = try await fetchUser(id: 1)
let posts = try await fetchPosts(for: user.id)

// Параллельный вызов: оба запроса выполняются одновременно
async let user = fetchUser(id: 1)
async let posts = fetchPosts(for: 1)
let (resultUser, resultPosts) = try await (user, posts)

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

Управление задачами: Task и TaskGroup

Task — это единица асинхронной работы. Вы создаете задачу, и она выполняется в фоновом потоке. С помощью TaskGroup можно управлять группой задач.

Создание и отмена задач

// Создание задачи
let task = Task {
    let result = await performHeavyComputation()
    return result
}

// Ожидание результата
let value = await task.value

// Отмена задачи
task.cancel()

// Проверка отмены внутри задачи
Task {
    for i in 0..<1000 {
        // Проверяем, не отменили ли задачу
        try Task.checkCancellation()
        // или
        if Task.isCancelled { break }
        await doWork()
    }
}

Отмена задач — важная фича. Например, когда пользователь переходит на другой экран, вы можете отменить загрузку данных, чтобы не тратить ресурсы.

TaskGroup: выполнение множества задач

func fetchAllUsers(ids: [Int]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask {
                return try await fetchUser(id: id)
            }
        }
        
        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

В этом примере мы запускаем загрузку всех пользователей параллельно. TaskGroup автоматически управляет потоками и собирает результаты.

Защита данных: Actor

Когда вы работаете с многопоточностью, возникает проблема состояния гонки — когда два потока одновременно изменяют одни и те же данные. Для защиты от этого в Swift есть Actor.

Actor — это ссылочный тип, который гарантирует, что только один поток может получить доступ к его свойствам и методам в любой момент времени.

actor BankAccount {
    private var balance: Double = 0
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func withdraw(amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
    
    func getBalance() -> Double {
        return balance
    }
}

// Использование
let account = BankAccount()

Task {
    await account.deposit(amount: 100)
    let balance = await account.getBalance()
    print("Баланс: \(balance)")
}

О

Блоги

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