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