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

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

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

Начать курс

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

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

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

В этом руководстве мы разберем ключевые концепции асинхронного программирования в Swift на практических примерах. Вы узнаете, как использовать async/await, Task, Actor и другие инструменты для создания быстрых и отзывчивых приложений под iOS и macOS.

Основные концепции Swift Concurrency

1. Async и Await: основа асинхронности

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

Рассмотрим простой пример: загрузка данных из сети.

import Foundation

// Объявляем асинхронную функцию
func fetchUserData() async throws -> String {
    let url = URL(string: "https://api.example.com/user/1")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8) ?? "Ошибка"
}

// Вызов асинхронной функции
Task {
    do {
        let userData = try await fetchUserData()
        print("Данные пользователя: \(userData)")
    } catch {
        print("Ошибка загрузки: \(error.localizedDescription)")
    }
}

Обратите внимание: try await означает, что мы ожидаем результат, который может выбросить ошибку. Код выглядит как обычный синхронный, но при этом не блокирует главный поток.

2. Task: точка входа в асинхронный мир

Асинхронные функции можно вызывать только из другого асинхронного контекста. Для запуска асинхронной операции из синхронного кода (например, из viewDidLoad()) используется Task.

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Запускаем асинхронную задачу
        Task {
            let data = await loadData()
            updateUI(with: data)
        }
    }
    
    func loadData() async -> String {
        // Имитация долгой загрузки
        try? await Task.sleep(nanoseconds: 2_000_000_000)
        return "Загруженные данные"
    }
    
    func updateUI(with data: String) {
        print("Обновляем UI: \(data)")
    }
}

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

3. Actor: защита от гонки данных

При работе с многопоточностью часто возникает проблема гонки данных (data race), когда несколько потоков одновременно изменяют одну переменную. Swift предлагает элегантное решение — Actor.

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

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

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

Task {
    await counter.increment()
    let currentValue = await counter.getValue()
    print("Текущее значение: \(currentValue)")
}

Обратите внимание: все вызовы методов Actor требуют await, так как они могут выполняться в другом потоке. Это обеспечивает безопасность данных на уровне компилятора.

Продвинутые техники асинхронного программирования

4. AsyncSequence: работа с потоками данных

Иногда данные поступают не единым блоком, а последовательно — например, при чтении файла построчно или получении уведомлений. Для таких случаев Swift предоставляет протокол AsyncSequence.

import Foundation

// Асинхронная последовательность для чтения строк из файла
func readLines(from url: URL) -> AsyncStream<String> {
    AsyncStream { continuation in
        Task {
            guard let fileHandle = try? FileHandle(forReadingFrom: url) else {
                continuation.finish()
                return
            }
            
            defer {
                try? fileHandle.close()
            }
            
            for try await line in fileHandle.bytes.lines {
                continuation.yield(line)
            }
            
            continuation.finish()
        }
    }
}

// Использование
Task {
    let fileURL = Bundle.main.url(forResource: "data", withExtension: "txt")!
    for await line in readLines(from: fileURL) {
        print("Строка: \(line)")
    }
}

AsyncStream позволяет создавать собственные асинхронные последовательности, а for await in — удобно их обрабатывать.

5. TaskGroup: параллельное выполнение задач

Когда нужно выполнить несколько независимых асинхронных операций одновременно, используйте TaskGroup. Это ускоряет работу в разы.

func loadAllImages(urls: [URL]) async throws -> [Data] {
    try await withThrowingTaskGroup(of: Data.self) { group in
        for url in urls {
            group.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                return data
            }
        }
        
        var results: [Data] = []
        for try await data in group {
            results.append(data)
        }
        return results
    }
}

// Использование
Task {
    let urls = [URL(string: "https://example.com/image1.png")!,
                URL(string: "https://example.com/image2.png")!]
    do {
        let images = try await loadAllImages(urls: urls)
        print("Загружено \(images.count) изображений")
    } catch {
        print("Ошибка загрузки: \(error)")
    }
}

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

Лучшие практики и част

Блоги

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