user7139248
user7139248

Reputation:

Cells reload wrong images when scrolled in UI Collection View

I'm downloading images using DropBox's API and displaying them in a Collection View. When the user scrolls, the image either disappears from the cell and another is loaded or a new image is reloaded and replaces the image in the cell. How can I prevent this from happening? I've tried using SDWebImage, this keeps the images in the right order but still the images disappear and reload each time they are scrolled off screen. Also, I'm downloading the images directly, not from a URL, I'd prefer to not have to write a work-a-round to be able to use SDWebImage.

I'd post a gif as example but my reputation is too low.

Any help would be welcomed :)

var filenames = [String]()
var selectedFolder = ""

    // image cache
    var imageCache = NSCache<NSString, UIImage>()

override func viewDidLoad() {
    super.viewDidLoad()

    getFileNames { (names, error) in
        self.filenames = names
        if error == nil {
            self.collectionView?.reloadData()
            print("Gathered filenames")
        }
    }

    collectionView?.collectionViewLayout = gridLayout
    collectionView?.reloadData()

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(true)

}


func getFileNames(completion: @escaping (_ names: [String], _ error: Error?) -> Void) {
    let client = DropboxClientsManager.authorizedClient!
    client.files.listFolder(path: "\(selectedFolder)", recursive: false, includeMediaInfo: true, includeDeleted: false, includeHasExplicitSharedMembers: false).response { response, error in
        var names = [String]()
        if let result = response {
            for entry in result.entries {
                if entry.name.hasSuffix("jpg") {
                    names.append(entry.name)

                }
            }
        } else {
            print(error!)
        }
        completion(names, error as? Error)
    }
}



func checkForNewFiles() {
    getFileNames { (names, error) in
        if names.count != self.filenames.count {
            self.filenames = names
            self.collectionView?.reloadData()
        }
    }
}

func downloadFiles(fileName: String, completion:@escaping (_ image: UIImage?, _ error: Error?) -> Void) {

    if let cachedImage = imageCache.object(forKey: fileName as NSString) as UIImage? {
        print("using a cached image")
        completion(cachedImage, nil)
    } else {
        let client = DropboxClientsManager.authorizedClient!
        client.files.download(path: "\(selectedFolder)\(fileName)").response { response, error in
            if let theResponse = response {
                let fileContents = theResponse.1
                if let image = UIImage(data: fileContents) {
                    // resize the image here and setObject the resized Image to save it to cache.
                    // use resized image for completion as well
                    self.imageCache.setObject(image, forKey: fileName as NSString)
                    completion(image, nil) // completion(resizedImage, nil)

                }
                else {
                    completion(nil, error as! Error?)
                }

            } else if let error = error {
                completion(nil, error as? Error)
            }
            }
            .progress { progressData in

        }
    }


}



override func numberOfSections(in collectionView: UICollectionView) -> Int {

    return 1
}


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

    return self.filenames.count
}


override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCell
    cell.backgroundColor = UIColor.lightGray

    let fileName = self.filenames[indexPath.item]
    let cellIndex = indexPath.item
    self.downloadFiles(fileName: fileName) { (image, error) in
        if cellIndex == indexPath.item {
          cell.imageCellView.image = image
          print("image download complete")

        }
    }

    return cell
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    gridLayout.invalidateLayout()
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    imageCache.removeAllObjects()
}

Upvotes: 2

Views: 3136

Answers (4)

Gianluigi Gamba
Gianluigi Gamba

Reputation: 1

I post this in case it may help someone. I have a collection view (displayed as a vertical list) whose items are collection views (displayed as horizontal single-line grids). Images in the child-collection views were repeated when the list was scrolled.

I solved it by placing this in the class of the cells of the parent collection view.

override func prepareForReuse() {
    collectionView.reloadData()
    super.prepareForReuse()
}

Upvotes: -2

user7139248
user7139248

Reputation:

I fixed it. It required setting the cell image = nil in the cellForItemAt func and canceling the image request if the user scrolled the cell off screen before it was finished downloading. Here's the new cellForItemAt code:

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let fileId = indexPath.item
    let fileName = self.filenames[indexPath.item]

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCell
    cell.backgroundColor = UIColor.lightGray
    if cell.request != nil {
        print("request not nil; cancel ", fileName)
    }

    cell.request?.cancel()
    cell.request = nil
    cell.imageCellView.image = nil

    print ("clear image ", fileId)
    self.downloadFiles(fileId:fileId, fileName: fileName, cell:cell) { (image, error) in

        guard let image = image else {
            print("abort set image ", fileId)
            return
        }

        cell.imageCellView.image = image

        print ("download/cache: ", fileId)
    }

    return cell
}    

Upvotes: 1

Aragunz
Aragunz

Reputation: 501

Use SDWebImage and add a placeholder image :

cell.imageView.sd_setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))

Upvotes: 0

Mr. James
Mr. James

Reputation: 486

Because TableView's and CollectionView's use the

dequeueReusableCell(withReuseIdentifier: for indexPath:) function when you configure a new cell, what swift does under the table is use a cell that is out of the screen to help the memory of your phone and probably that cell already has a image set and you have to handle this case.

I suggest you to look at the method "prepareCellForReuse" in this case what I think you have to do is set the imageView.image atribute to nil.

I have pretty sure that it will solve your problem or give you the right direction, but if it doesn't work please tell me and I will try to help you.

Best results.

Upvotes: 1

Related Questions