Avatar ·

Загрузка изображения из URL на Swift: как правильно и асинхронно?

Всем привет! Я новичок в Swift и iOS-разработке. Пишу приложение, которое показывает список товаров с картинками. Картинки грузятся из интернета по URL. Я написал простую функцию, но она работает очень странно: интерфейс зависает на несколько секунд, а иногда изображения вообще не отображаются. Вот мой код: ```swift func loadImage(from urlString: String) -> UIImage? { guard let url = URL(string: urlString) else { return nil } if let data = try? Data(contentsOf: url) { return UIImage(data: data) } return nil } ``` Я вызываю эту функцию прямо в `cellForRowAt` для каждой ячейки таблицы. Видимо, это плохая практика? Ещё я заметил, что при быстром скролле изображения начинают «скакать» — показываются не те картинки, которые нужно. Подскажите, как правильно организовать загрузку изображений из сети на Swift? Нужно ли использовать какие-то сторонние библиотеки или можно обойтись встроенными средствами? Хочется понять самый правильный и современный подход (Swift 5+, iOS 13+). Заранее спасибо за помощь! <<>>

Проблема синхронной загрузки

Ваш код использует Data(contentsOf:) — это синхронный вызов, который блокирует главный поток. Пока изображение загружается (даже 0.5 секунды), интерфейс полностью замирает: не работает скролл, не обрабатываются касания. В современных приложениях все сетевые запросы должны выполняться асинхронно.

Правильное решение: URLSession + кеширование

Лучший способ — использовать URLSession.shared.dataTask в комбинации с URLCache для кеширования. Вот готовая функция-расширение для UIImageView:

import UIKit

extension UIImageView {
    func loadImage(from urlString: String, placeholder: UIImage? = nil) {
        // Показываем заглушку, пока грузится
        self.image = placeholder
        
        guard let url = URL(string: urlString) else { return }
        
        // Используем URLCache для кеширования
        let cache = URLCache.shared
        let request = URLRequest(url: url)
        
        // Проверяем, есть ли изображение в кеше
        if let cachedData = cache.cachedResponse(for: request)?.data,
           let cachedImage = UIImage(data: cachedData) {
            DispatchQueue.main.async {
                self.image = cachedImage
            }
            return
        }
        
        // Асинхронная загрузка
        URLSession.shared.dataTask(with: request) { data, response, error in
            guard let data = data, 
                  let image = UIImage(data: data),
                  let response = response,
                  error == nil else { return }
            
            // Сохраняем в кеш
            let cachedResponse = CachedURLResponse(response: response, data: data)
            cache.storeCachedResponse(cachedResponse, for: request)
            
            // Возвращаемся на главный поток для обновления UI
            DispatchQueue.main.async {
                self.image = image
            }
        }.resume()
    }
}

Как использовать в UITableView/UICollectionView

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ProductCell
    let product = products[indexPath.row]
    
    // Сбрасываем старое изображение (чтобы не было "скачков" при реюзе)
    cell.productImageView.image = nil
    cell.productImageView.loadImage(from: product.imageURL, placeholder: UIImage(named: "placeholder"))
    
    return cell
}

Почему это работает правильно?

  • Асинхронность — загрузка не блокирует UI
  • Кеширование — повторная загрузка того же URL происходит мгновенно из кеша
  • Безопасность потоков — обновление UI только на главном потоке
  • Предотвращение "скачков" — сброс image перед новой загрузкой

Продвинутый вариант: async/await (iOS 15+)

Если ваше приложение поддерживает iOS 15+, можно использовать современный синтаксис:

extension UIImageView {
    func loadImageAsync(from urlString: String) async {
        guard let url = URL(string: urlString) else { return }
        
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            if let image = UIImage(data: data) {
                await MainActor.run {
                    self.image = image
                }
            }
        } catch {
            print("Ошибка загрузки: \(error)")
        }
    }
}

// Использование:
Task {
    await cell.productImageView.loadImageAsync(from: product.imageURL)
}

Когда стоит использовать сторонние библиотеки?

Для продакшн-проектов часто используют Kingfisher или SDWebImage. Они предоставляют:

  • Автоматическое кеширование на диск и в память
  • Отмену загрузок при скролле
  • Поддержку GIF, SVG и других форматов
  • Простое API: imageView.kf.setImage(with: url)

Однако для обучения и небольших проектов встроенное решение на URLSession — это отличный выбор.

<<>>

Альтернативный подход: OperationQueue и отмена загрузок

Если вы не хотите использовать расширения UIImageView, можно создать отдельный менеджер загрузки с возможностью отмены операций при быстром скролле:

class ImageLoader {
    private let queue = OperationQueue()
    private var operations: [URL: BlockOperation] = [:]
    
    func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
        // Отменяем предыдущую загрузку для этого URL
        operations[url]?.cancel()
        
        let operation = BlockOperation { [weak self] in
            guard let data = try? Data(contentsOf: url),
                  let image = UIImage(data: data) else {
                DispatchQueue.main.async { completion(nil) }
                return
            }
            DispatchQueue.main.async { completion(image) }
        }
        
        operations[url] = operation
        queue.addOperation(operation)
    }
    
    func cancelAll() {
        queue.cancelAllOperations()
        operations.removeAll()
    }
}

Используйте этот менеджер в контроллере. При скролле вызывайте cancelAll() в scrollViewDidEndDragging или при перезагрузке данных. Это предотвратит загрузку изображений для ячеек, которые уже ушли с экрана. Однако помните: такой подход всё ещё использует синхронный Data(contentsOf:) внутри операции, поэтому для больших файлов лучше комбинировать с URLSession.