Проблема с загрузкой и кэшированием изображений

Я загружаю изображения с сервера и показываю их в collectionView. Я кеширую изображения, чтобы пользователь получил быстрый ответ сервера и не было сбоев в пользовательском интерфейсе. Пока изображение не загружено, я также добавил изображение-заполнитель.

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

Вот код ниже

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    @IBOutlet weak var colView: UICollectionView!

    var imageCache = NSCache<NSString, UIImage>()
    var arrURLs = [
        "https://homepages.cae.wisc.edu/~ece533/images/airplane.png",
        "https://homepages.cae.wisc.edu/~ece533/images/arctichare.png",
        "https://homepages.cae.wisc.edu/~ece533/images/baboon.png",
        "https://homepages.cae.wisc.edu/~ece533/images/barbara.png",
        "https://homepages.cae.wisc.edu/~ece533/images/boat.png",
        "https://homepages.cae.wisc.edu/~ece533/images/cat.png",
        "https://homepages.cae.wisc.edu/~ece533/images/fruits.png",
        "https://homepages.cae.wisc.edu/~ece533/images/frymire.png",
        "https://homepages.cae.wisc.edu/~ece533/images/girl.png",
        "https://homepages.cae.wisc.edu/~ece533/images/goldhill.png",
        "https://homepages.cae.wisc.edu/~ece533/images/lena.png",
        "https://homepages.cae.wisc.edu/~ece533/images/monarch.png",
        "https://homepages.cae.wisc.edu/~ece533/images/mountain.png",
        "https://homepages.cae.wisc.edu/~ece533/images/peppers.png",
        "https://homepages.cae.wisc.edu/~ece533/images/pool.png",
        "https://homepages.cae.wisc.edu/~ece533/images/sails.png",
        "https://homepages.cae.wisc.edu/~ece533/images/serrano.png",
        "https://homepages.cae.wisc.edu/~ece533/images/tulips.png",
        "https://homepages.cae.wisc.edu/~ece533/images/watch.png",
        "https://homepages.cae.wisc.edu/~ece533/images/zelda.png"
    ]


func downloadImage(url: URL, imageView: UIImageView, placeholder : UIImage) {

    imageView.image = placeholder // Set default placeholder..

    // Image is set if cache is available
    if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) {
        imageView.image = cachedImage
    } else {
        // Reset the image to placeholder as the URLSession fetches the new image
        imageView.image = placeholder
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard error == nil else  {
                // You should be giving an option to retry the image here
                imageView.image = placeholder
                return
            }

            if let respo  = response as? HTTPURLResponse {

                print("Status Code : ", respo.statusCode)

                if let imageData = data, let image = UIImage(data: imageData) {
                    self.imageCache.setObject(image, forKey: url.absoluteString as NSString)
                    // Update the imageview with new data
                    DispatchQueue.main.async {
                        imageView.image = image
                    }
                } else {
                    // You should be giving an option to retry the image here
                    imageView.image = placeholder
                }
            }
            }.resume()
    }
}


    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let w = self.view.bounds.width - 30

        return CGSize(width: w, height: w + 60)
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return arrURLs.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DummyCollectionViewCell", for: indexPath) as! DummyCollectionViewCell

        let str = arrURLs[indexPath.item]
        let url = URL(string: str)

        downloadImage(url: url!) { (img) in
            DispatchQueue.main.async {
                cell.imgView.image = img ?? UIImage(named: "placeholder")
            }
        }

        return cell
    }
}

Выходной GIF

Проблема с загрузкой и кэшированием изображений


Из-за ограничения размера стека изображение выше имеет низкое качество. Если вам нужно проверить gif в полном размере, см.: https://imgur.com/tcjMWgc

Установите местозаполнитель UIImageView в операторе else метода downloadImage

Cerlin 29.07.2019 12:00

@CerlinBoss В какой строке вы говорите?

dahiya_boy 29.07.2019 12:03

Я добавил это как ответ

Cerlin 29.07.2019 12:08

вы можете использовать фреймворк SDWebImage, он все сделает за вас

Alexandr Kolesnik 29.07.2019 13:04

@AlexandrKolesnik Вот что я пытаюсь научиться работать с NSCache. Есть ли у вас какое-либо решение по этой проблеме, поскольку я уже пробовал миллионы решений и комбинаций, которые нашел в Google.

dahiya_boy 29.07.2019 13:10

все ячейки можно использовать повторно, поэтому вы должны установить изображение по умолчанию в cellForRow перед установкой изображения из кеша, чем вы должны связать свою ячейку.

Alexandr Kolesnik 29.07.2019 13:13

@AlexandrKolesnik Можешь перепроверить обновленный downloadImage func. Я делаю то же самое, что вы сказали. Если чего-то все еще не хватает, дайте мне знать, я буду рад попробовать ваш ответ.

dahiya_boy 29.07.2019 13:16
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
750
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Это потому, что ячейка многоразовая.

Верхняя ячейка используется повторно, но ячейка не обновляет изображение, поскольку изображение ячейки уже установлено.

Вы должны расширить UIImage для обновления изображения ячейки

как это:

расширение UIImageView {

func loadImageNone(_ urlString: String) {

    if let cacheImage = imageCache.object(forKey: urlString as NSString) {
        self.run(with: cacheImage)
        return
    } else {
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard error == nil else  {
                completion(nil)
                return
            }

            if let respo  = response as? HTTPURLResponse {
                if let imageData = data, let image = UIImage(data: imageData) {

                  imageCache.setObject(image, forKey: urlString as NSString)

                  DispatchQueue.main.async {
                         self.image = image
                  }
                }
            }
        }.resume()
   }

func run(with image: UIImage) {
    UIView.transition(with: self,
                      duration: 0.5,
                      options: [],
                      animations: { self.image = image },
                      completion: nil)
     }
}

Причина, по которой вы упомянули, верна, но простая анимация смены изображения не решает проблему. Также оператор if не требует возврата, поскольку после оператора else нет кода.

Cerlin 29.07.2019 12:02

Общая разница ч/б этого ответа и моего заключается в том, что я делаю не так, как в расширении. Остальная часть кода такая же. P.S. Я также попробовал ответ Shobhakar Tiwari от здесь, который точно такой же, как ваш ответ.

dahiya_boy 29.07.2019 12:05

Измените свой метод, как показано ниже

// This method is getting called for all the cells
func downloadImage(url: URL, imageView: UIImageView) {
    // Image is set if cache is available
    if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) {
        imageView.image = cachedImage
    } else {
        // Reset the image to placeholder as the URLSession fetches the new image
        imageView.image = UIImage(named: "placeholder")
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard error == nil else  {
                // You should be giving an option to retry the image here
                imageView.image = UIImage(named: "placeholder")
                return
            }

            if let respo  = response as? HTTPURLResponse {
                print("Status Code : ", respo.statusCode)
                if let imageData = data, let image = UIImage(data: imageData) {
                    self.imageCache.setObject(image, forKey: url.absoluteString as NSString)
                    // Update the imageview with new data 
                    imageView.image = image
                } else {
                    // You should be giving an option to retry the image here
                    imageView.image = UIImage(named: "placeholder")
                }
            }
        }.resume()
    }
}

И назовите это внутри cellForItemAt лайком

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DummyCollectionViewCell", for: indexPath) as! DummyCollectionViewCell

    let str = arrURLs[indexPath.item]

    if let url = URL(string: str) {
        downloadImage(url: url, imageView: cell.imgView)
    } else {
        cell.imgView.image = UIImage(named: "placeholder")
    }

    return cell
}

Я изменил свой код на ваш и все равно получаю тот же ответ. Многие ответы и блоги имеют тот же ответ, что и мы, но не знаем, откуда взялся этот сбой.

dahiya_boy 29.07.2019 12:19

@dahiya_boy Код, который вы разместили в вопросе, имеет проблему. Оператор else будет ожидать завершения URLSession для установки изображения (пока это не произойдет, будет отображаться старое изображение из повторно используемой ячейки.)

Cerlin 29.07.2019 12:22

о, да, это один из пунктов. Согласен, но после добавления вашего кода все равно есть глюки. Как только все изображения загружаются в кеш, все работает правильно. Таким образом, может возникнуть проблема со временем получения изображения с сервера. Кстати, если я перезапущу приложение, то я получу значение из кеша или нет, или мне нужно явно хранить кеш, чтобы сохранить изображения в кеше?

dahiya_boy 29.07.2019 12:26

Вы должны вызвать prepareForReuse() с super для пользовательского класса UICollectionViewCell. Это гарантирует, что удаление из очереди вызывается для каждой строки и получает кеш.

override func prepareForReuse() {
    super.prepareForReuse()

    reuseAction()
}

Из документа Apple Также, когда изображение загружается, вы должны:

self.collectionView.reloadData()

или перезагрузить строку, если вы держите ссылку на строку, когда изображение заканчивает загрузку

let indexSet = IndexSet(integer: indexPath.section)
collectionView.reloadSections(indexSet)

Не могли бы вы объяснить, какое отношение ваш ответ имеет к моему вопросу?

dahiya_boy 29.07.2019 13:11
Ответ принят как подходящий

Я думаю, проблема в вашем обработчике ответов, вы устанавливаете кеш для запрашиваемого URL-адреса, а не для URL-адреса из ответа, я немного изменил ваш код, попробуйте, надеюсь, это поможет вам

func downloadImage(url: URL, imageView: UIImageView, placeholder: UIImage? = nil, row: Int) {
    imageView.image = placeholder
    imageView.cacheUrl = url.absoluteString + "\(row)"
    if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) {
        imageView.image = cachedImage
    } else {
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard
                let response = response as? HTTPURLResponse,
                let imageData = data,
                let image = UIImage(data: imageData),
                let cacheKey = response.url?.absoluteString,
                let index = self.arrURLs.firstIndex(of: cacheKey)
                else { return }
            DispatchQueue.main.async {
                if cacheKey + "\(index)" != imageView.cacheUrl { return }
                imageView.image = image
                self.imageCache.setObject(image, forKey: cacheKey as NSString)
            }
            }.resume()
    }
}

И

var associateObjectValue: Int = 0
extension UIImageView {

    fileprivate var cacheUrl: String? {
        get {
            return objc_getAssociatedObject(self, &associateObjectValue) as? String
        }
        set {
            return objc_setAssociatedObject(self, &associateObjectValue, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
    }
}

ОБНОВЛЕНО:

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

dahiya_boy 29.07.2019 13:48

ты заменил свой код на мой? попробуйте переустановить приложение, чтобы очистить кеш перед использованием

Alexandr Kolesnik 29.07.2019 13:54

Можете ли вы объяснить, что было не так? И я также хотел знать, что мы используем кеш для хранения временных значений. Поэтому каждый раз, когда я запускаю изображение, его кеш очищается, а изображения загружаются повторно. Но в SDWebImage, как только изображение добавляется в кеш, оно остается в кеше до тех пор, пока мы явно не очистим его. Почему?

dahiya_boy 29.07.2019 14:30

Как я уже сказал выше, ячейка может использоваться повторно, как и все ее содержимое. При отладке я заметил, что imageView с одним и тем же адресом загружает разные URL-адреса, поэтому я добавил переменную времени выполнения для обновления URL-адреса кеша в соответствии со строкой ячейки. Вы можете посмотреть код SDWebImage, я думаю, что они архивируют кеш

Alexandr Kolesnik 29.07.2019 14:33
USE THIS IMAGE LOADER EXTENSION 

let imageCache = NSCache<AnyObject, AnyObject>()

class ImageLoader: UIImageView {

    var imageURL: URL?

    let activityIndicator = UIActivityIndicatorView()

    func loadImageWithUrl(_ url: URL) {

        // setup activityIndicator...
        activityIndicator.color = .darkGray

        addSubview(activityIndicator)
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true

        imageURL = url

        image = nil
        activityIndicator.startAnimating()

        // retrieves image if already available in cache
        if let imageFromCache = imageCache.object(forKey: url as AnyObject) as? UIImage {

            self.image = imageFromCache
            activityIndicator.stopAnimating()
            return
        }

        // image does not available in cache.. so retrieving it from url...
        URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in

            if error != nil {
                print(error as Any)
                self.activityIndicator.stopAnimating()
                return
            }

            DispatchQueue.main.async(execute: {

                if let unwrappedData = data, let imageToCache = UIImage(data: unwrappedData) {

                    if self.imageURL == url {
                        self.image = imageToCache
                    }

                    imageCache.setObject(imageToCache, forKey: url as AnyObject)
                }
                self.activityIndicator.stopAnimating()
            })
        }).resume()
    }
}

            ** design controller  **

                 import UIKit

                class ImageController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

            private let cellId = "cellId"

                lazy var imagesSliderCV: UICollectionView = {

                    let layout = UICollectionViewFlowLayout()
                    layout.scrollDirection = .vertical
                    let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
                    cv.translatesAutoresizingMaskIntoConstraints = false
                    cv.backgroundColor = .white
                    cv.showsHorizontalScrollIndicator = false
                    cv.delegate = self
                    cv.dataSource = self
                    cv.isPagingEnabled = true
                    cv.register(ImageSliderCell.self, forCellWithReuseIdentifier: self.cellId)
                    return cv
                }()

             //
                // Mark:- CollectionView Methods........
                //
                var arrURLs = [
                    "https://homepages.cae.wisc.edu/~ece533/images/airplane.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/arctichare.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/baboon.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/barbara.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/boat.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/cat.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/fruits.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/frymire.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/girl.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/goldhill.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/lena.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/monarch.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/mountain.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/peppers.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/pool.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/sails.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/serrano.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/tulips.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/watch.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/zelda.png"
                ]

                func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

                    return arrURLs.count
                }

                func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! ImageSliderCell


                    let ImagePath = arrURLs[indexPath.item]
                       if  let strUrl = ImagePath.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed),
                        let imgUrl = URL(string: strUrl) {

                        cell.frontImg.loadImageWithUrl(imgUrl)
                    }
                    return cell
                }

                func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

                    return CGSize(width: screenWidth, height: 288)
                }

                func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {

                    return 0
                }


        func setupAutoLayout(){

                NSLayoutConstraint.activate([

                    imagesSliderCV.leftAnchor.constraint(equalTo: view.leftAnchor),
                    imagesSliderCV.rightAnchor.constraint(equalTo: view.rightAnchor),
                    imagesSliderCV.topAnchor.constraint(equalTo: view.topAnchor),
                    imagesSliderCV.bottomAnchor.constraint(equalTo: view.bottomAnchor),

                    ])

            }
        }

    **collectionView cell **

    import UIKit

    class ImageSliderCell: UICollectionViewCell {    

        //
        let frontImg: ImageLoader = {

            let img = ImageLoader()
            img.translatesAutoresizingMaskIntoConstraints = false
            img.contentMode = .scaleAspectFill
            img.clipsToBounds = true
            return img
        }()

        //
        override init(frame: CGRect) {
            super.init(frame: frame)

            addSubview(frontImg)
            setupAutolayout()
        }

        func setupAutolayout(){

            frontImg.leftAnchor.constraint(equalTo: leftAnchor, constant: 8).isActive = true
            frontImg.rightAnchor.constraint(equalTo: rightAnchor, constant: -8).isActive = true
            frontImg.topAnchor.constraint(equalTo: topAnchor, constant: 8).isActive = true
            frontImg.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8).isActive = true
        }

        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }

ВЫХОД: -

Другие вопросы по теме