swift iOS - 快速滚动后 UICollectionView 图像混淆

Posted

技术标签:

【中文标题】swift iOS - 快速滚动后 UICollectionView 图像混淆【英文标题】:swift iOS - UICollectionView images mixed up after fast scroll 【发布时间】:2016-10-13 10:52:49 【问题描述】:

我是 swift 和 ios 编程的新手,但我已经能够在 Xcode 中为我的应用程序构建一个基本稳定的启动界面。 CollectionView 从我的家庭网络服务器上的 csv 文件创建的字典数组中抓取图像和文本。 cvs 文件包含以下格式的数据(注意,url 已更改以保护许可图像):

csv 文件

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

问题是当在 CollectionView 中快速滚动时,单元格会抓取不正确的图像。值得注意的是,如果您进行慢速或中等滚动,则会在正确的图像渲染到正确的单元格之前显示不同的图像(单元格的标签文本始终正确,只有图像永远关闭)。发生图像与带标签的正确单元格不匹配后,CollectionView 中的所有其他单元格也会显示不正确的图像。

例如单元格 1-9 将显示 Title1-9 和正确的 Image1-9 缓慢滚动时,单元格 19-27 将显示标题 19-27,将短暂显示图像 10-18,然后显示正确的图像 19-27。 当快速滚动大量单元格时(例如从单元格 1-9 到单元格 90-99),单元格 90-99 将显示标题 90-99,将显示图像 10-50ish,然后将错误地停留在图像 41-50 (或附近)。进一步滚动时,单元格 100+ 将显示正确的标题,但仅显示图像 41-50 范围内的图像。

我认为这个错误要么是因为单元重用没有正确处理,要么是因为图像缓存没有正确处理,或者两者兼而有之。这也可能是我作为初学者 iOS/swift 程序员看不到的东西。我尝试使用完成修饰符实现请求,但似乎无法按照我的代码设置方式使其正常工作。我将不胜感激这方面的任何帮助以及解释为什么修复程序会以这种方式工作。谢谢!

相关代码如下。

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!


【问题讨论】:

单元被重用,并且您正在异步调度图像提取,因此当您使用单元并异步获取新图像时,旧的提取仍在运行。你需要处理这个。可能最简单的方法是使用 SDWebImage,它在 UIImageView 上有一个扩展,可以为你处理这个 @Paulw11:是的,我认为这与未正确取消请求或其他原因有关。我希望仅使用 UIKit 编写代码,因为我将此应用程序用作学习体验,并且我觉得依赖外部框架会破坏该目的,因为它是一种快捷方式。不过,我确实研究了 Alamofire/AlamofireImage 和 SDWebImage,但由于某种原因,在安装 pod 后,我无法导入模块(这是一个我还没有完全弄清楚的单独问题)。如果需要,我会使用它,只是想先尝试用 UIKit 处理它。 另一种方法是将图像 URL 存储为单元格的属性,然后在完成闭包中,根据您刚刚获取的 URL 检查属性值。如果不匹配,则不要设置图像,因为该单元格已被重复使用。 【参考方案1】:

您需要了解 dequeue 如何正常工作。这方面有很好的文章。

总结一下:

为了保持滚动流畅度,使用了 dequeue,它本质上是 在一定限制后重复使用单元格。假设您有 10 个可见单元格 时间,它可能会创建 16-18(上面 3-4,下面 3-4,只是粗糙 估计)只有细胞,即使您可能需要 1000 个细胞。

现在当你这样做时-

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

您正在从已创建的单元格中重用现有单元格。这就是为什么您会在一段时间内看到旧图像,然后在加载后看到新图像。

您需要在出列单元格后立即清除旧图像。

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

您实际上将在图像中以这种方式设置一个空白占位符,而不是在您的情况下将 imageCache 设置为 nil。

解决了这个问题-

值得注意的是,如果您进行慢速或中速滚动,则会在正确的图像被渲染到正确的单元格之前显示不同的图像(单元格的标签文本始终正确,只有图像永远关闭)。

现在在为当前单元格分配图像时,您需要再次检查您分配的图像是否属于该单元格。您需要检查这一点,因为您分配给它的图像视图正在被重复使用,并且它可能与 indexPath 处的单元格相关联,该 indexPath 与生成图像加载请求时的位置不同。

当您将单元格出列并将 image 属性设置为 nil 时,请让 seriesImage 为您记住当前的 indexPath。

cell.seriesImage.indexPath = indexPath

稍后,如果 imageView 仍然属于先前分配的 indexPath,则只需将图像分配给它。这 100% 可以重复使用单元格。

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

您可能需要考虑在 UIImageView 实例上设置 indexPath。这是我为类似场景准备并使用的东西- UIView-Additions

它在 Objective-C 和 Swift 中都可用。

【讨论】:

所以经过一些测试,一旦我将单元格出列就清除旧图像似乎会扰乱图像缓存。它确实解决了滚动时单元格中显示多个图像的问题,但它破坏了已经滚动到的图像的缓存。例如,如果我向上滚动,图像现在是黑色的,即使通过刷新发送 reloaddata() 信号也不会加载。至于检查属于该单元格的图像,您能再扩展一下吗?我查看了您的 UIView Additions swift 文件,但鉴于我的代码结构,我不确定如何实现它。 当你出列一个单元格时,你将 indexPath 分配给 imageView 实例,在稍后的阶段,你在分配图像之前检查这个值。我现在正在更新答案。 谢谢!这个周末我会试试这个并报告(我的编码是一个副兴趣) 我得到了错误,value of UIImageView has no member indexPath 这样做:覆盖 func prepareForReuse() cellImageView.image = nil super.prepareForReuse() 【参考方案2】:

在您的自定义单元格类中实现func prepareForReuse()。并在 prepareForReuse() 函数中重置您的图像。所以它会在重用单元格之前重置图像视图。

【讨论】:

您能对此进行扩展或澄清吗?当我在自定义单元格类中的“func prepareForReuse()”方法中重置图像时,当我滚动回这些图像时,所有滚动传递的图像都是黑色 (nil)。 覆盖 func prepareForReuse() super.prepareForReuse() yourImageView.image = UIImage() 谢谢!这个周末我会试试这个。

以上是关于swift iOS - 快速滚动后 UICollectionView 图像混淆的主要内容,如果未能解决你的问题,请参考以下文章

UITableViewCell 图像在上下滚动后发生变化,但 TextLabel 文本保持不变(iOS8,Swift)

Swift ios导航栏在被滚动隐藏后不会出现

无法在 swift 中使用滚动视图滚动图像

Swift - 如果项目滚动一点,UICollection 快速恢复

滚动后的 Collectionview 项目位置在 iPhone 4s 中未正确显示

快速滚动时带有 AlamofireImage 的 Swift UItableview 崩溃