Search code examples
iosswiftuitableviewsdwebimage

UITableview, SDWebImage, cell reuse and UIImage problems


See video example here: https://i.sstatic.net/A93hm.jpg

Have a tableview with images im downloading asynchronously from the internet. Im able to get them to populate correctly and without issue with either my own code or SDWebImage. My problem begins when I paginate more data to the end of the tableview. Pagination does work. As does all the text data, it all works and remains correctly throughout the tableview. The problem im having is with my ImageView. As you can see in the video, after pagination, the UIImageViews image changes all the cells from the previous call to what appears to be the image of whats in the most recent cell at the end of the tableview. What is driving this behavior and why is it only happening to my imageviews and what can I do to fix it?

ive tried setting the imageview image nil in prepareForReuse (against Apples recommendations) and it produces the same result.

Relevant code

API Call

 func downloadGamesByPlatformIDJSON(platformID: Int?, fields: String?, include: String?, pageURL: String?, completed: @escaping () -> () ) {
        var urlString : String?
        
        if pageURL == nil {
            urlString = "https://api.thegamesdb.net/v1/Games/ByPlatformID?apikey=\(apiKey)&id=\(platformID!)"
            
            if fields != nil {
                urlString = urlString! + "&fields=" + fields!
            }
            if include != nil {
                
                urlString = urlString! + "&include=" + include!
            }
            
        } else {
            urlString = pageURL!
        }
        let url = URL(string: "\(urlString!)")!
        var requestHeader = URLRequest.init(url: url)
        requestHeader.httpMethod = "GET"
        requestHeader.setValue("application/json", forHTTPHeaderField: "Accept")
        
        URLSession.shared.dataTask(with: requestHeader) { (data, response, error) in
            
            if error != nil {
                print("error = \(error)")
                completed()
            }
            
            if error == nil {
                do {
                    print("error = nil")
                    let json = String(data: data!, encoding: .utf8)
                    
                    print(json)
                    
                    if let jsonDecodedPlatforms = try JSONDecoder().decode(ByPlatformIDData?.self, from: data!) {
                        let decodedJSON = jsonDecodedPlatforms.data?.games
                        self.boxart = jsonDecodedPlatforms.include.boxart
                        self.baseURL = jsonDecodedPlatforms.include.boxart.baseURL
                        self.page = jsonDecodedPlatforms.pages
                        
     
                        self.games.append(contentsOf: decodedJSON!                
                            }
                    
                    DispatchQueue.main.async {
                        completed()
                    }
                } catch {
                    print(error)
                }
                
            }
            
        }.resume()
        
    }

Pagination

func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        let contentHeight = scrollView.contentSize.height
        
        if offsetY > contentHeight - scrollView.frame.size.height {
            
            if !fetchingMore {
                beginBatchFetch() 
            }
        }
    }
    func beginBatchFetch() {
        fetchingMore = true
        print("fetching data")
        network.downloadGamesByPlatformIDJSON(platformID: nil, fields: nil, include: nil, pageURL: network.page?.next) {
            print("pagination successful")
            self.fetchingMore = false
            self.tableView.reloadData()
                      
                  }
        
    }

cellForRowAt

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ViewControllerTableViewCell
        
        cell.configureCells()

        //creating game object
        let game1 : GDBGamesPlatform?
        game1 = network.games[indexPath.row]

 //retrieving the filename information based on the game id
        
        if network.boxart?.data["\(game1!.id!)"]?[0].side == .front {
            print(network.boxart?.data["\(game1!.id!)"]?[0].filename)
            frontImageName = network.boxart?.data["\(game1!.id!)"]?[0].filename as! String
            
        } else if network.boxart?.data["\(game1!.id!)"]?[0].side == .back {
            backImageName = network.boxart?.data["\(game1!.id!)"]?[0].filename as! String
            
        }
        
        if network.boxart?.data["\(game1!.id!)"]?.count == 2 {
        if network.boxart?.data["\(game1!.id!)"]?[1].side == .front {
            frontImageName = network.boxart?.data["\(game1!.id!)"]?[1].filename as! String
                       
        } else if network.boxart?.data["\(game1!.id!)"]?[1].side == .back {
            backImageName = network.boxart?.data["\(game1!.id!)"]?[1].filename as! String
                       
        }
        }
   
        
        
        //creating image url string
        var imageUrlString = network.baseURL!.small + frontImageName
        print(imageUrlString)
        let imageURL = URL(string: imageUrlString)
        
        
        
        
        //if data exists for the front cover image download it, otherwise show default image
        if frontImageName != nil {
            
            cell.loadCoverImageWith(urlString: imageUrlString)
            
        } else {
            cell.tableViewCoverImage.image = UIImage(named: "noArtNES")

        }
        
        return cell
        
    }

import UIKit
import SDWebImage

class ViewControllerTableViewCell: UITableViewCell {
    @IBOutlet weak var tableViewCoverImage: UIImageView!
    @IBOutlet weak var tableViewGameName: UILabel!
    @IBOutlet weak var tableViewGenreLabel: UILabel!
    @IBOutlet weak var tableViewAgeRatingLabel: UILabel!
    @IBOutlet weak var tableViewCompanyLabel: UILabel!
    @IBOutlet weak var tableViewReleaseDateLabel: UILabel!
    @IBOutlet weak var backgroundCell: UIView!
    @IBOutlet weak var tableViewCoverRearImage: UIImageView!
    @IBOutlet weak var gameCartImage: UIImageView!
    
    
    override func prepareForReuse() {
           super.prepareForReuse()
        
        tableViewCoverImage.sd_cancelCurrentImageLoad()
        tableViewCoverImage.image = nil
             }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
        
                                                  
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
    func loadCoverImageWith(urlString: String) {
        let imageURL = URL(string: urlString)
       
            
            self.tableViewCoverImage.sd_setImage(with: imageURL, placeholderImage: UIImage(named: "noArtNES"), options: SDWebImageOptions.highPriority) { (image, error, cacheType, url) in
                
                if let error = error {
                    print("Error downloading the image.  Error Description: \(error.localizedDescription)")
                } else {
                    let edgeColor = self.tableViewCoverImage.image?.edgeColor()
                    self.tableViewCoverImage.layer.shadowColor = edgeColor!.cgColor
                }
            }

        
        
        
    }
    

    func configureCells() {
        tableViewCoverImage.layer.shadowOffset = CGSize(width: -5, height: 8)
        tableViewCoverImage.layer.shadowRadius = 8
        tableViewCoverImage.layer.shadowOpacity = 0.8
        tableViewCoverImage.layer.cornerRadius = 10
        tableViewCoverImage.clipsToBounds = false
        tableViewCoverImage.layer.masksToBounds = false
        backgroundCell.layer.shadowOffset = CGSize(width: 0, height: 5)
        backgroundCell.layer.shadowRadius = 5
        backgroundCell.layer.shadowOpacity = 0.1
        backgroundCell.layer.cornerRadius = 10
        if self.traitCollection.userInterfaceStyle == .light {
            backgroundCell.layer.shadowColor = UIColor.black.cgColor
            backgroundCell.layer.backgroundColor = UIColor.white.cgColor

        } else {
            backgroundCell.layer.shadowColor = UIColor.white.cgColor
            backgroundCell.layer.backgroundColor = UIColor.black.cgColor

    }
    
   

}
}

Solution

  • The second and later calls to the API are appending the results for games to the array but overwriting the results for boxart. The results for the later pages will not include the boxart from earlier pages.

    So, your boxart data no longer includes the art for earlier pages. The reason you are getting the image from the last item, is that the variables frontImageName and backImageName have the wrong scope, they should be local to tableView(:cellForRowAt:)

    You need to merge the results of the API call with existing boxart dictionary by calling Dictionary.merge(_:uniquingKeysWith:). See https://developer.apple.com/documentation/swift/dictionary/3127171-merge for details