Введение в асинхронное программирование в 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)")
}
О