Храните один URLSessionDataTask на URL и несколько подписчиков. Отмена удаляет одного подписчика; сеть отменяется, только когда больше никто не ждёт результат.
Модуль загрузки изображений
Храните один URLSessionDataTask на URL и несколько подписчиков. Отмена удаляет одного подписчика; сеть отменяется, только когда больше никто не ждёт результат.
Короткий ответ
Что ответить на собеседовании
Подробный разбор
Ответ с пояснениями
Условие
Задача: разработать и реализовать сервис, отвечающий за загрузку изображений. Сервис должен принимать запросы на получение изображения и отдавать его клиенту. Основные требования:
- Возможность прервать запрос изображения.
- При одновременных запросах одного и того же ресурса несколько запросов должны быть объединены в один сетевой запрос. Ограничение: использовать нельзя Swift Concurrency и Combine.
Ответ
Храните один URLSessionDataTask на URL и несколько подписчиков. Отмена удаляет одного подписчика; сеть отменяется, только когда больше никто не ждёт результат.
Минимальный вариант для одинаковых GET-запросов без Swift Concurrency и Combine:
import UIKit
final class ImageLoader {
struct Token { let url: URL; let id: UUID }
enum Failure: Error { case invalidResponse }
typealias Completion = (Result<UIImage, Error>) -> Void
private struct Entry {
let generation: UUID
let task: URLSessionDataTask
var callbacks: [UUID: Completion]
}
private let queue = DispatchQueue(label: "image.loader.state")
private var entries: [URL: Entry] = [:]
func load(_ url: URL, completion: @escaping Completion) -> Token {
let token = Token(url: url, id: UUID())
queue.sync {
if entries[url] != nil {
entries[url]?.callbacks[token.id] = completion
return
}
let generation = UUID()
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
let result: Result<UIImage, Error>
if let error { result = .failure(error) }
else if let http = response as? HTTPURLResponse,
(200..<300).contains(http.statusCode),
let data, let image = UIImage(data: data) {
result = .success(image)
} else { result = .failure(Failure.invalidResponse) }
guard let self else { return }
self.queue.async {
guard let entry = self.entries[url],
entry.generation == generation else { return }
self.entries[url] = nil
for callback in entry.callbacks.values {
DispatchQueue.main.async { callback(result) }
}
}
}
entries[url] = Entry(generation: generation, task: task,
callbacks: [token.id: completion])
task.resume()
}
return token
}
func cancel(_ token: Token) {
queue.sync {
guard var entry = entries[token.url] else { return }
entry.callbacks[token.id] = nil
if entry.callbacks.isEmpty {
entries[token.url] = nil
entry.task.cancel()
} else { entries[token.url] = entry }
}
}
}
Отмена действует до передачи результата в main queue; уже запланированный callback здесь не отзывается, поэтому ячейка проверяет актуальность URL/токена. Generation защищает новый запрос от позднего completion старого. Для production добавьте лимит размера, кеш и корректный ключ с параметрами авторизации; при Swift 6 отдельно оформите Sendable/изоляцию, не отключая проверки без анализа.