10

I am new to swift and iOS programming, but I have been able to build a mostly stable starting interface for my application in Xcode. The CollectionView grabs an image and text from an array of dictionaries created from a csv file on my home network server. The cvs file contains data in the following format (note, urls have been changed to protect licensed images):

csv file

csv file is @ url https://myserver.com/csv.txt and contains the following

Title";"SeriesImageURL
Title1";"https://licensedimage.com/url1
Title2";"https://licensedimage.com/url2
...
Title1000";"https://licensedimage.com/url1000

The problem is that when scrolling quickly through the CollectionView, the cell will grab the incorrect image. Noticeably, if you do a slow or medium scroll, a different image will show before the correct image is rendered into the correct cell (the label text for the cells are always correct, only the image is ever off). After the mismatch of images to proper cell with label occurs, all other cells in the CollectionView will also have incorrect images displayed.

E.g. Cell 1-9 will show Title1-9 with correct Image1-9 When scrolling slowly, Cells 19-27 will show Title 19-27, will briefly show Image 10-18 and then show the correct Image 19-27. When scrolling quickly a huge number of cells (e.g. from cell 1-9 to cell 90-99), Cells 90-99 will show Title 90-99, will show Image 10-50ish, and then will incorrectly stay on Image 41-50(or thereabout). When scrolling further, Cells 100+ will display the correct Title but will only show images from the range Image 41-50.

I think this error is either because the cell reuse isn't handled properly, the caching of images isn't handled properly, or both. It could also be something I am not seeing as a beginner iOS/swift programmer. I have tried to implement a request with a completion modifier but cannot seem to get it working properly with the way my code is set up. I would appreciate any help with this as well as an explanation for why the fix works the way it does. Thanks!

The relevant code is below.

SeriesCollectionViewController.swift

class SeriesCollectionViewController: UICollectionViewController, UISearchBarDelegate {

let reuseIdentifier:String = "SeriesCell"

// Set Data Source Models & Variables
struct seriesModel {

    let title: AnyObject
    let seriesimageurl: AnyObject
}
var seriesDict = [String:AnyObject]()
var seriesArray = [seriesModel]()

// Image Cache
var imageCache = NSCache() 

override func viewDidLoad() {
    super.viewDidLoad()
    // Grab Data from Source
    do {

        let url = NSURL(string: "https://myserver.com/csv.txt")
        let fullText = try NSString(contentsOfURL: url!, encoding: NSUTF8StringEncoding)
        let readings = fullText.componentsSeparatedByString("\n") as [String]
        var seriesDictCount = readings.count
        seriesDictCount -= 1
        for i in 1..<seriesDictCount {
            let seriesData = readings[i].componentsSeparatedByString("\";\"")
            seriesDict["Title"] = "\(seriesData[0])"
            seriesDict["SeriesImageURL"] = "\(seriesData[1])"
            seriesArray.append(seriesModel(
                title: seriesDict["Title"]!,
                seriesimageurl: seriesDict["SeriesImageURL"]!,
            ))
        }
    } catch let error as NSError {
        print("Error: \(error)")
    }
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    imageCache.removeAllObjects()
    // Dispose of any resources that can be recreated.
}

//...
//...skipping over some stuff that isn't relevant
//...

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> SeriesCollectionViewCell {
    let cell: SeriesCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! SeriesCollectionViewCell

    if (self.searchBarActive) {
        let series = seriesArrayForSearchResult[indexPath.row]
        do {
            // set image
            if let imageURL = NSURL(string: "\(series.seriesimageurl)") {
                if let image = imageCache.objectForKey(imageURL) as? UIImage {
                        cell.seriesImage.image = image
                } else {
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {
                        if let tvimageData = NSData(contentsOfURL: imageURL) {
                            let image = UIImage(data: tvimageData)
                            self.imageCache.setObject(image!, forKey: imageURL)
                                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                                    cell.seriesImage.image = nil
                                    cell.seriesImage.image = image
                                })
                        }
                    })
                }
            }
            cell.seriesLabel.text = "\(series.title)"
        }
    } else {
        let series = seriesArray[indexPath.row]
        do {
            // set image
            if let imageURL = NSURL(string: "\(series.seriesimageurl)") {
                if let image = imageCache.objectForKey(imageURL) as? UIImage {
                        cell.seriesImage.image = image
                } else {
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {
                        if let tvimageData = NSData(contentsOfURL: imageURL) {
                            let image = UIImage(data: tvimageData)
                            self.imageCache.setObject(image!, forKey: imageURL)
                            dispatch_async(dispatch_get_main_queue(), { () -> Void in
                                cell.seriesImage.image = nil
                                cell.seriesImage.image = image
                            })
                        }
                    })
                }

            }
            cell.seriesLabel.text = "\(series.title)"
        }
    }
    cell.layer.shouldRasterize = true
    cell.layer.rasterizationScale = UIScreen.mainScreen().scale
    cell.prepareForReuse()
    return cell
}

SeriesCollectionViewCell

class SeriesCollectionViewCell: UICollectionViewCell {

@IBOutlet weak var seriesImage: UIImageView!
@IBOutlet weak var seriesLabel: UILabel!

}
shadowofjazz
  • 117
  • 1
  • 5
  • Cells are reused and your are dispatching the image fetch asynchronously, so when you ruse the cell and asynchronously fetch a new image then the old fetch is still running. You need to handle this. Probably the easiest way is to use something SDWebImage which has an extension on UIImageView that will handle this for you – Paulw11 Jun 13 '16 at 06:06
  • @Paulw11: Yea, I figured it had to do with the requests not being canceled properly or something. I was hoping to write the code just using UIKit since I'm using this application as a learning experience and I feel like relying on an external framework defeats that purpose since it's a shortcut. I did actually look into Alamofire/AlamofireImage and SDWebImage, though, but for some reason after the pods are installed, I'm not able to import in the modules (this is a separate issue I haven't quite figured out). I'll use that if I need to though, just want to try handling it with UIKit first. – shadowofjazz Jun 13 '16 at 06:17
  • Another approach is store the image URL as a property of your cell, then in the completion closure, check the property value against the URL you just fetched. If it doesn't match then don't set the image as the cell has already been reused. – Paulw11 Jun 13 '16 at 06:19

2 Answers2

15

You need to understand how dequeue works properly. There are very good articles for this.

To summarise:

To maintain scrolling smoothness, dequeue is used which essentially reuses cells after a certain limit. Say you have 10 visible cells at a time, it will likely create 16-18 (3-4 above, 3-4 below, just rough estimates) cells only, even though you might need 1000 cells.

Now when you are doing this-

let cell: SeriesCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! SeriesCollectionViewCell

You are reusing an existing cell from already made cells. That's why you see old image for some time and then you see new image once it's loaded.

You need to clear the old image as soon as you dequeue the cell.

cell.seriesImage.image = UIImage()    //nil

You will essentially set a blank placeholder this way in the image and not mess with imageCache being nil in your case.

That solves this problem-

Noticeably, if you do a slow or medium scroll, a different image will show before the correct image is rendered into the correct cell (the label text for the cells are always correct, only the image is ever off).

Now when assigning the image for current cell, you need to check again, if the image you're assigning belongs to this cell or not. You need to check this because the image view you're assigning it to is being reused and it might be associated to a cell at an indexPath different from that of when the image load request was generated.

When you a dequeue the cell and set the image property to nil, ask the seriesImage to remember the current indexPath for you.

cell.seriesImage.indexPath = indexPath

Later, you only assign the image to it, if the imageView still belongs to previously assigned indexPath. This works 100% with cell reuse.

if cell.seriesImage.indexPath == indexPath
cell.seriesImage.image = image

You might need to consider setting the indexPath on UIImageView instance. Here's something I prepared and use for a similar scenario- UIView-Additions

It is available in both Objective-C & Swift.

Tarun Tyagi
  • 9,364
  • 2
  • 17
  • 30
  • So after some testing, clearing the old image as soon as I dequeue the cell seems to upset the image caching. It does fix the problem of multiple images shown in a cell while scrolling, but it breaks the caching of images that were already scrolled to. E.g., if I scroll back up the images are now black and won't be loaded even with a reloaddata() signal sent via refresh. As to the check for the image belonging to the cell, can you expand on that a little bit more? I took a look at your UIView Additions swift file but I'm not sure how to implement it given the structure of my code. – shadowofjazz Jun 15 '16 at 01:55
  • When you dequeue a cell, you assign the indexPath to imageView instance, at a later stage, you check this value before assigning the image. I'm updating the answer now. – Tarun Tyagi Jun 15 '16 at 02:32
  • Thanks! I will try this out this weekend and report back (my coding is a side interest) – shadowofjazz Jun 16 '16 at 03:16
  • I get the error, `value of UIImageView has no member indexPath` – Konstantinos Natsios Aug 10 '16 at 15:26
  • do like this : override func prepareForReuse() { cellImageView.image = nil super.prepareForReuse() } – Chandramani May 28 '17 at 03:18
  • Thanks a lot Tarun, you are a legend. Love from Pakistan ;) – Ali Aug 16 '17 at 11:02
4

Implement func prepareForReuse() in your custom cell class. And reset your image in prepareForReuse() function. so it will reset the image view before reuse the cell.

Arun Kumar P
  • 820
  • 2
  • 12
  • 25