Avatar ·

Загрузка и отображение изображения по URL в Swift (UIImage)

Привет! Я только начал изучать Swift и пытаюсь сделать простое приложение, которое показывает картинку из интернета. У меня есть URL строки с изображением, например: `"https://example.com/image.jpg"`. Я пытаюсь загрузить это изображение и показать его в `UIImageView`. Вот мой код: ```swift let url = URL(string: "https://example.com/image.jpg") let data = try? Data(contentsOf: url!) imageView.image = UIImage(data: data!) ``` Но это не работает — приложение зависает на несколько секунд, а потом вылетает с ошибкой. Я читал, что `Data(contentsOf:)` может блокировать главный поток, но как правильно загрузить картинку? Я слышал про `URLSession`, но не понимаю, как его использовать. Подскажите, пожалуйста, как правильно и безопасно загрузить изображение из сети в Swift? Мне нужно, чтобы интерфейс не зависал, и была обработка ошибок (например, если нет интернета или URL неверный). Буду благодарен за пример с кодом! <<>>

Проблема вашего подхода

Вы правы: Data(contentsOf:) выполняет синхронную загрузку на главном потоке, что приводит к зависанию UI. Кроме того, принудительное извлечение опционалов с помощью ! может вызвать краш, если URL неверный или данные не загрузились. Правильное решение — использовать URLSession для асинхронной загрузки и обработки ошибок.

Решение с URLSession (рекомендуется)

Создайте вспомогательную функцию или extension для UIImageView:

import UIKit

extension UIImageView {
    func loadImage(from urlString: String, placeholder: UIImage? = nil) {
        // Показываем placeholder, пока загружается
        self.image = placeholder
        
        guard let url = URL(string: urlString) else {
            print("Ошибка: неверный URL - \(urlString)")
            return
        }
        
        // Асинхронная загрузка
        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            // Проверяем ошибки
            if let error = error {
                print("Ошибка загрузки: \(error.localizedDescription)")
                return
            }
            
            // Проверяем статус ответа (необязательно, но полезно)
            guard let httpResponse = response as? HTTPURLResponse,
                  (200...299).contains(httpResponse.statusCode) else {
                print("Ошибка: неверный HTTP-статус")
                return
            }
            
            // Конвертируем данные в UIImage
            guard let data = data, let image = UIImage(data: data) else {
                print("Ошибка: не удалось создать UIImage из данных")
                return
            }
            
            // Обновляем UI на главном потоке
            DispatchQueue.main.async {
                self?.image = image
            }
        }.resume() // Важно! Не забудьте вызвать resume()
    }
}

Использование

let imageView = UIImageView()
imageView.loadImage(from: "https://example.com/image.jpg", placeholder: UIImage(named: "default"))

Ключевые моменты:

  • Асинхронность: dataTask выполняется в фоне, не блокируя UI
  • Безопасность опционалов: используем guard let вместо !
  • Обработка ошибок: проверяем URL, данные, HTTP-статус
  • Главный поток: обновляем UIImageView только через DispatchQueue.main.async
  • Слабая ссылка: [weak self] предотвращает утечки памяти

Продвинутый вариант: кэширование

Чтобы не загружать одно и то же изображение повторно, добавьте простой кэш:

let cache = NSCache<NSString, UIImage>()

extension UIImageView {
    func loadImageCached(from urlString: String) {
        let cacheKey = NSString(string: urlString)
        
        // Проверяем кэш
        if let cachedImage = cache.object(forKey: cacheKey) {
            self.image = cachedImage
            return
        }
        
        guard let url = URL(string: urlString) else { return }
        
        URLSession.shared.dataTask(with: url) { data, _, error in
            guard let data = data, let image = UIImage(data: data), error == nil else { return }
            
            // Сохраняем в кэш
            cache.setObject(image, forKey: cacheKey)
            
            DispatchQueue.main.async {
                self.image = image
            }
        }.resume()
    }
}

Теперь ваше приложение не будет зависать, а картинки будут загружаться корректно. Для продакшена рекомендую использовать готовые библиотеки (например, Kingfisher или SDWebImage), которые делают всё это автоматически.

<<>>

Альтернативный подход: Combine (iOS 13+)

Если вы используете современные версии iOS, можно применить фреймворк Combine для реактивной загрузки:

import UIKit
import Combine

class ViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!
    private var cancellable: AnyCancellable?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        loadImageWithCombine()
    }
    
    func loadImageWithCombine() {
        guard let url = URL(string: "https://example.com/image.jpg") else { return }
        
        cancellable = URLSession.shared.dataTaskPublisher(for: url)
            .map { UIImage(data: $0.data) }
            .replaceError(with: nil)
            .receive(on: DispatchQueue.main)
            .assign(to: \.image, on: imageView)
    }
}

Что здесь происходит:

  • dataTaskPublisher — асинхронный publisher, работающий в фоне
  • map преобразует данные в UIImage (или nil при ошибке)
  • replaceError заменяет ошибку на nil, чтобы избежать краша
  • receive(on:) переключает на главный поток для обновления UI
  • assign автоматически устанавливает изображение в UIImageView

Этот подход более декларативный и хорошо сочетается с архитектурой MVVM. Однако требует iOS 13+ и понимания Combine. Для простых задач лучше использовать решение с URLSession из первого ответа.