Модуль загрузки изображений

Храните один URLSessionDataTask на URL и несколько подписчиков. Отмена удаляет одного подписчика; сеть отменяется, только когда больше никто не ждёт результат.

Короткий ответ

Что ответить на собеседовании

Храните один URLSessionDataTask на URL и несколько подписчиков. Отмена удаляет одного подписчика; сеть отменяется, только когда больше никто не ждёт результат.

Подробный разбор

Ответ с пояснениями

Условие

Задача: разработать и реализовать сервис, отвечающий за загрузку изображений. Сервис должен принимать запросы на получение изображения и отдавать его клиенту. Основные требования:

  1. Возможность прервать запрос изображения.
  2. При одновременных запросах одного и того же ресурса несколько запросов должны быть объединены в один сетевой запрос. Ограничение: использовать нельзя 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/изоляцию, не отключая проверки без анализа.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее