Код-ревью Senior Swift Concurrency обновлено

Код-ревью: загрузчик картинок на Swift Concurrency

Вопрос

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

final class ImageLoader {
    static let shared = ImageLoader()
    private var cache: [URL: UIImage] = [:]

    func image(for url: URL) async throws -> UIImage {
        if let cached = cache[url] { return cached }
        let (data, _) = try await URLSession.shared.data(from: url)
        let image = UIImage(data: data)!
        cache[url] = image
        return image
    }
}

final class AvatarView: UIImageView {
    func show(_ url: URL) {
        Task.detached {
            let img = try? await ImageLoader.shared.image(for: url)
            self.image = img
        }
    }
}

Коротко

Правильная версия: actor ImageLoader с NSCache и словарём [URL: Task<UIImage, Error] для слияния одинаковых запросов. Во view: @MainActor, обычный Task { } (наследует главный актор), ссылка на задачу в свойстве, cancel() в prepareForReuse и проверка, что URL всё ещё актуален, перед присваиванием.

Что должен найти ревьюер

  1. Ошибкастрока 3
    cache это изменяемый словарь в классе, к которому обращаются из разных задач одновременно: гонка данных. Нужен actor или защита блокировкой.
  2. Производительностьстрока 6
    Два одновременных запроса одного URL оба скачают картинку. Стоит хранить выполняющиеся Task по URL и переиспользовать их.
  3. Ошибкастрока 8
    UIImage(data:)! упадёт на битых данных или HTML-странице ошибки. Нужно бросить ошибку.
  4. Производительностьстрока 3
    Кеш без ограничения размера и без реакции на нехватку памяти. Лучше NSCache с лимитом.
  5. Ошибкастрока 18
    Task.detached не наследует MainActor, и self.image меняется не на главном потоке.
  6. Ошибкастрока 16
    Задача не сохраняется и не отменяется: при переиспользовании ячейки старая загрузка может перезаписать новую картинку.

Разбор

Правильная версия: actor ImageLoader с NSCache и словарём [URL: Task<UIImage, Error>] для слияния одинаковых запросов. Во view: @MainActor, обычный Task { } (наследует главный актор), ссылка на задачу в свойстве, cancel() в prepareForReuse и проверка, что URL всё ещё актуален, перед присваиванием. Ошибку декодирования картинки превращаем в свою ошибку, а не крэш.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.