CollectionView duplicate cell when loading more data

Mat0 picture Mat0 · Nov 12, 2015 · Viewed 8.3k times · Source

The problem:

I have a CollectionView which loads a UIimage into each cell. However my problem is that when I load additional cells with more images they appear to duplicate. I can't quite see what could be causing this in my code. Might it be because of some problem with reusable cells?

Can anyone see why this occurs?

NOTE: The array with the images doesn't have duplicates

Video of problem: https://www.youtube.com/watch?v=vjRsFc8DDmI

Image of the problem:

CollectionView has duplicate cell when loading more data

Here are my functions for collectionView:

func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    //#warning Incomplete method implementation -- Return the number of items in the section
    if self.movies == nil
    {
      return 0

    }

    return self.movies!.count

}

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

   if self.movies != nil && self.movies!.count >= indexPath.row
    {
        // Calc size of cell
        cell.frame.size.width = screenWidth / 3
        cell.frame.size.height = screenWidth / 3 * 1.54

        let movies = self.movies![indexPath.row]
        if(movies.posterPath != "" || movies.posterPath != "null"){
        cell.data = movies.posterPath
        }
        else{
        cell.data = nil
        }
        // See if we need to load more movies
        let rowsToLoadFromBottom = 5;
        let rowsLoaded = self.movies!.count
        if (!self.isLoadingMovies && (indexPath.row >= (rowsLoaded - rowsToLoadFromBottom)))
        {
            let totalRows = self.movieWrapper!.totalResults!
            let remainingMoviesToLoad = totalRows - rowsLoaded;
            if (remainingMoviesToLoad > 0)
            {
                self.loadMoreMovies()
            }
        }
}
   else
{
        cell.data = nil
}

    return cell
}

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
    return CGSize(width: screenWidth/3, height: screenWidth/3*1.54)
}

Here I load the data from a Wrapper class:

func loadFirstMovies()
{

    isLoadingMovies = true

    Movies.getMovies({ (movieWrapper, error) in
        if error != nil
        {
            // TODO: improved error handling
            self.isLoadingMovies = false
            let alert = UIAlertController(title: "Error", message: "Could not load first movies \(error?.localizedDescription)", preferredStyle: UIAlertControllerStyle.Alert)
            alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.Default, handler: nil))
            self.presentViewController(alert, animated: true, completion: nil)
        }
        self.addMoviesFromWrapper(movieWrapper)
        self.activityIndicator.hidden = true
        self.isLoadingMovies = false
        self.collectionView.reloadData()
    })
}

func loadMoreMovies(){

    self.isLoadingMovies = true
    if self.movies != nil && self.movieWrapper != nil && self.movieWrapper!.page < self.movieWrapper!.totalPages
    {
        // there are more species out there!
        Movies.getMoreMovies(self.movieWrapper, completionHandler: { (moreWrapper, error) in
            if error != nil
            {
                // TODO: improved error handling
                self.isLoadingMovies = false
                let alert = UIAlertController(title: "Error", message: "Could not load more movies \(error?.localizedDescription)", preferredStyle: UIAlertControllerStyle.Alert)
                alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.Default, handler: nil))
                self.presentViewController(alert, animated: true, completion: nil)
            }
            print("got more!")
            self.addMoviesFromWrapper(moreWrapper)
            self.isLoadingMovies = false
            self.collectionView.reloadData()
        })
    }
}

func addMoviesFromWrapper(wrapper: MovieWrapper?)
{
    self.movieWrapper = wrapper
    if self.movies == nil
    {
        self.movies = self.movieWrapper?.results
    }
    else if self.movieWrapper != nil && self.movieWrapper!.results != nil
    {
        self.movies = self.movies! + self.movieWrapper!.results!
    }
}

And lastly I call: loadFirstMovies() in viewDidLoad()

EDIT: The UpcomingCollectionViewCell

class UpcomingCollectionViewCell: UICollectionViewCell {

@IBOutlet weak var imageView: UIImageView!


var data:String?{
    didSet{
        self.setupData()
    }
}

func setupData(){

   self.imageView.image = nil // reset the image

    if let urlString = data{
        let url =  NSURL(string: "http://image.tmdb.org/t/p/w342/" + urlString)
        self.imageView.hnk_setImageFromURL(url!)


      }
    }  
 }

Answer

Duncan C picture Duncan C · Nov 12, 2015

This is a typical table view/collection view setup issue.

Any time you recycle a cell using a dequeue method like dequeueReusableCellWithReuseIdentifier:, you must always fully configure all the views in the cell, including setting all the text fields/image views to their starting values. Your code has several if statements where, if the condition of the if is false, you don't set up the views in the cell. You need to have else clauses that clear out old content from the cell's views in case there is content left over from the last time the cell was used.

**EDIT:

Change your cellForItemAtIndexPath method to begin like this:

func collectionView(collectionView: UICollectionView, 
  cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell 
{
    let cell =    
      collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, 
        forIndexPath: indexPath) as! UpcomingCollectionViewCell
    cell.imageView.image = nil;  //Remove the image from the recycled cell
//The rest of your method ...