UITableview、SDWebImage、cell复用和UIImage问题

Posted

技术标签:

【中文标题】UITableview、SDWebImage、cell复用和UIImage问题【英文标题】:UITableview, SDWebImage, cell reuse and UIImage problems 【发布时间】:2020-09-12 15:25:29 【问题描述】:

在此处查看视频示例: https://imgur.com/a/vSBPjlP

有一个表格视图,其中包含从 Internet 异步下载的图像。我能够让它们正确填充,并且我自己的代码或 SDWebImage 都没有问题。当我将更多数据分页到表格视图的末尾时,我的问题就开始了。分页确实有效。与所有文本数据一样,它们都可以正常工作并在整个表格视图中保持正确。我遇到的问题是我的 ImageView。正如您在视频中看到的那样,在分页之后,UIImageViews 图像将所有单元格从上一次调用更改为看起来是表格视图末尾最新单元格中的内容的图像。是什么导致了这种行为,为什么它只发生在我的图像视图中,我可以做些什么来解决它?

我尝试将 prepareForReuse 中的 imageview 图像设置为零(违反 Apple 的建议),它产生了相同的结果。

相关代码

API 调用

 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()
        
    

分页

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

    
    
   



【问题讨论】:

后续页面的url没有设置include参数。看起来这导致盒子艺术被空内容覆盖。还要检查 frontImageName 和 backImageName 的范围。它们应该是 tableView(:cellForRowAt:) 的本地 @Dale 在 JSON 响应中返回后续页面的 URL,并包含原始调用中的所有参数。 JSON 中的 url 看起来像:'https://api.thegamesdb.net/v1/Games/ByPlatformID?apikey=MYAPIKEY&id=7&fields=players%2Cpublishers%2Cgenres%2Coverview%2Clast_updated%2Crating%2Cplatform%2Ccoop%2Cyoutube%2Cos%2Cprocessor %2Cram%2Chdd%2Cvideo%2Csound%2Calternates&include=boxart%2Cplatform&page=2 ' @Dale 很有趣,将前后 ImageName 的范围更改为本地使之前的图像现在默认为分页后的占位符图像。 后续页面返回的boxart是否返回之前页面的boxart?可能不是。您正在附加游戏的结果,但设置 boxart 的结果。 @Dale 我认为您在这里有所了解,并且一段时间以来它一直在我脑海中逐渐消失。我的问题是,在 JSON 响应中,boxarts 作为字典而不是像游戏数据这样的数组返回。如果返回的不是数组,我如何获取返回的盒子艺术数据并附加它?以下是返回的 JSON 示例:pastebin.com/5bV6UTcx 【参考方案1】:

对 API 的第二次和以后的调用会将游戏结果附加到数组中,但会覆盖 boxart 的结果。后面页面的结果将不包括前面页面的 boxart。

因此,您的 boxart 数据不再包含早期页面的艺术作品。您从最后一项获取图像的原因是变量 frontImageName 和 backImageName 的范围错误,它们应该是 tableView(:cellForRowAt:) 的本地变量

您需要通过调用 Dictionary.merge(_:uniquingKeysWith:) 将 API 调用的结果与现有的 boxart 字典合并。详情见https://developer.apple.com/documentation/swift/dictionary/3127171-merge

【讨论】:

【参考方案2】:

这是一个可重用性问题。尝试在 cellForRowAtIndex 中更改这一行。

tableview.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "cell\(indexPath.row)")
let cell = tableView.dequeueReusableCell(withIdentifier: "cell\(indexPath.row)", for: indexPath) as! ViewControllerTableViewCell

【讨论】:

这会导致崩溃,因为没有名为 cell0、cell1、cell2 等的单元格。单元格标识符是单元格。如何更改每个单元格的标识符,如果我每次更改标识符都不会破坏单元格可重用的目的? 我已经更新了答案。我假设您知道单元格必须使用我们出列的相同 ID 进行注册。 谢谢。我目前在 IB 中将我的重用标识符设置为“单元格”。那应该改变吗?我可以让它运行的唯一方法是将代码中的重用标识符设置为“单元格”。如果 indexPath.row 期望“单元格”,它将如何输入? 这似乎是个坏主意。如果您有大量数据,这将导致内存问题,因为您不再重用任何单元格。您正在为每一行初始化一个新单元格。

以上是关于UITableview、SDWebImage、cell复用和UIImage问题的主要内容,如果未能解决你的问题,请参考以下文章

SDWebImage 和 UITableViewCell

SDWebImage - 如何从缓存加载图像?

SDWebImage使用,图片加载和缓存

UITableView调优

解决方法UITableView 性能优化笔记

UITableview 不向上滚动