Загрузка изображения из URL на Swift: как правильно и асинхронно?
Проблема синхронной загрузки
Ваш код использует 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.
Ответов пока нет.
Войдите чтобы оставить ответ