← Назад к вопросам

Как добиться выполнения кода после получения данных от нескольких URL-запросов?

2.0 Middle🔥 211 комментариев
#Многопоточность и асинхронность#Работа с сетью

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Синхронизация нескольких асинхронных URL-запросов в iOS

В iOS-разработке существует несколько эффективных подходов для синхронизации выполнения кода после получения данных от нескольких асинхронных URL-запросов. Рассмотрим основные методы от простых к сложным.

1. DispatchGroup — классический подход

DispatchGroup из фреймворка Grand Central Dispatch (GCD) позволяет группировать задачи и дожидаться их завершения:

let group = DispatchGroup()
var results: [String: Data] = [:]
let urls = [
    "https://api.example.com/data1",
    "https://api.example.com/data2",
    "https://api.example.com/data3"
]

for urlString in urls {
    group.enter()
    
    guard let url = URL(string: urlString) else {
        group.leave()
        continue
    }
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        defer { group.leave() }
        
        if let data = data {
            results[urlString] = data
        }
    }.resume()
}

group.notify(queue: .main) {
    // Все запросы завершены
    print("Все данные получены: \(results.count) элементов")
    // Выполняем финальный код
    self.processAllData(results)
}

2. Async/Await (Swift 5.5+) — современный подход

С появлением async/await код стал значительно чище и читабельнее:

func fetchMultipleURLs() async throws -> [Data] {
    let urls = [
        URL(string: "https://api.example.com/data1")!,
        URL(string: "https://api.example.com/data2")!,
        URL(string: "https://api.example.com/data3")!
    ]
    
    // Параллельное выполнение запросов
    async let firstData = fetchData(from: urls[0])
    async let secondData = fetchData(from: urls[1])
    async let thirdData = fetchData(from: urls[2])
    
    // Ожидание завершения всех запросов
    let results = try await [firstData, secondData, thirdData]
    return results
}

private func fetchData(from url: URL) async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

// Использование
Task {
    do {
        let allData = try await fetchMultipleURLs()
        await MainActor.run {
            // Обновление UI на главном потоке
            self.updateUI(with: allData)
        }
    } catch {
        print("Ошибка: \(error)")
    }
}

3. OperationQueue с зависимостями

Для сложных сценариев с зависимостями между запросами подходит OperationQueue:

let queue = OperationQueue()
var operations: [Operation] = []
var results: [Data] = []

for urlString in urls {
    let operation = BlockOperation {
        // Синхронный запрос внутри операции
        if let url = URL(string: urlString),
           let data = try? Data(contentsOf: url) {
            results.append(data)
        }
    }
    operations.append(operation)
}

let completionOperation = BlockOperation {
    // Выполняется после всех операций
    DispatchQueue.main.async {
        self.handleCompletion(results: results)
    }
}

operations.forEach { completionOperation.addDependency($0) }
queue.addOperations(operations + [completionOperation], waitUntilFinished: false)

4. Combine Framework — реактивный подход

Для проектов, использующих Combine, есть элегантное решение:

import Combine

func fetchMultipleRequests() {
    let urls = [
        URL(string: "https://api.example.com/data1")!,
        URL(string: "https://api.example.com/data2")!
    ]
    
    let publishers = urls.map { url in
        URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .catch { _ in Just(Data()) }
    }
    
    Publishers.MergeMany(publishers)
        .collect() // Ждем все результаты
        .receive(on: DispatchQueue.main)
        .sink { allData in
            // Все данные получены
            self.processData(allData)
        }
        .store(in: &cancellables)
}

5. Собственные реализации с семафорами

Для низкоуровневого контроля можно использовать DispatchSemaphore:

let semaphore = DispatchSemaphore(value: 0)
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
var results: [Data] = []
var errors: [Error] = []

for url in urls {
    concurrentQueue.async {
        URLSession.shared.dataTask(with: url) { data, _, error in
            if let data = data {
                results.append(data)
            } else if let error = error {
                errors.append(error)
            }
            semaphore.signal()
        }.resume()
        semaphore.wait()
    }
}

// Все запросы завершены
DispatchQueue.main.async {
    self.handleResults(results, errors: errors)
}

Критерии выбора подхода

  • Для iOS 13+ / Swift 5.5+: Используйте async/await — это наиболее современный и читаемый подход
  • Для поддержки старых версий: DispatchGroup или OperationQueue
  • Для реактивных архитектур: Combine Framework
  • Для максимальной производительности: Параллельные запросы с async let или concurrent DispatchQueue
  • Для сложных зависимостей: OperationQueue с установкой зависимостей между операциями

Важные considerations

  1. Обработка ошибок: Всегда предусматривайте сценарии частичного успеха
  2. Отмена операций: Реализуйте механизмы отмены долгих запросов
  3. Очереди: Помните о потоках выполнения — UI обновляйте только на главном потоке
  4. Производительность: Ограничивайте количество одновременных запросов (обычно 4-6)
  5. Память: Учитывайте рост потребления памяти при параллельной загрузке больших данных

Современная рекомендация — использовать async/await где возможно, так как этот подход сочетает производительность, читаемость кода и безопасность работы с памятью.