无法重新排序 tableView 单元格图像

Posted

技术标签:

【中文标题】无法重新排序 tableView 单元格图像【英文标题】:Failling to reorder tableView cell images 【发布时间】:2020-09-01 22:10:47 【问题描述】:

出于学习目的,我正在创建一个应用程序来显示一些星球大战舰艇的列表。它为船对象获取我的 json(本地)(在本例中它有 4 艘船)。 它使用自定义单元格作为表格视图。

如果我已经下载了图像(在用户文档中),表格填充没有问题。 我的 starshipData 数组由我的 DataManager 类通过委托填充。 我删除了一些代码以使类更小,如果需要,我可以显示所有内容。

好的,所以当我按下排序按钮时(很少发生)问题。 我这样做的方式是在恢复或下载图​​像后,更新 starshipData 数组中的图像字段。

这是我的排序方法,非常基本。

@objc private func sortByCost(sender: UIBarButtonItem) 
        starshipData.sort  $0.costInCredits < $1.costInCredits 
        starshipTableView.reloadData()

这里是 tableView 的实现。

首先我使用 cellForRowAt 方法填充快速/轻量数据。

    // MARK: -> cellForRowAt
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
        let cell = tableView.dequeueReusableCell(withIdentifier: "StarshipCell", for: indexPath) as! StarshipCell
        let starship = starshipData[indexPath.row]
        
        // update cell properties
        cell.starshipNameLabel.text = starship.name
        cell.starshipManufacturerLabel.text = starship.manufacturer
        cell.starshipCostLabel.text = currencyFormatter(value: starship.costInCredits)

        // only populate the image if the array has one (the first time the table is populated, 
        // the array doesn't have an image, it'll need to download or fetch it in user documents) 
        if starship.image != nil 
            cell.starshipImgView.image = starship.image
        
        
        // adds right arrow indicator on the cell
        cell.accessoryType = .disclosureIndicator
        
        return cell
    

这里我使用 willDisplay 方法来下载或获取图像,基本上是较重的数据。

    // MARK: -> willDisplay
    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) 
        // update cell image
        let cell = cell as! StarshipCell
        let imageUrl = starshipData[indexPath.row].imageUrl
        let starshipName = starshipData[indexPath.row].name
        let index = indexPath.row
        
        // if there isn't any image on the cell, proceed to manage the image
        if cell.starshipImgView.image == nil 
            // only instantiate spinner on imageView position if no images are set
            let spinner = UIActivityIndicatorView(style: .medium)
            startSpinner(spinner: spinner, cell: cell)

            // manage the image
            imageManager(starshipName: starshipName, imageUrl: imageUrl, spinner: spinner, cell: cell, index: index)  (image) in
                self.addImageToCell(cell: cell, spinner: spinner, image: image)
            
        
    

这就是我认为问题所在,因为我对 swift 和后台线程的了解仍在开发中。

我通过打印日志发现单元格未显示正确图像的时间是因为数组没有该索引的图像,因此单元格显示上次填充/加载表格时的图像.

我想知道是不是因为在用户按下排序按钮之前后台线程没有足够的时间用获取/下载的图像更新 starshipArray。

问题是,如果第一次正确填充表格,当按下排序按钮时,starshipData 数组应该已经包含所有图像,正如您在 imageManager 方法中看到的那样,在图像解包FromDocuments之后,我调用updateArrayImage 更新图像。

也许是使用的 dispatchesQueues 的数量?完成处理程序和 dispatchQueues 使用是否正确?

    private func imageManager(starshipName: String, imageUrl: URL?, spinner: UIActivityIndicatorView, cell: StarshipCell, index: Int, completion: @escaping (UIImage) -> Void) 
        // if json has a string on image_url value
        if let unwrappedImageUrl = imageUrl 
            // open a background thread to prevent ui freeze
            DispatchQueue.global().async 
                // tries to retrieve the image from documents folder
                let imageFromDocuments = self.retrieveImage(imageName: starshipName)
                
                // if image was retrieved from folder, upload it
                if let unwrappedImageFromDocuments = imageFromDocuments 


// TO FORCE THE PROBLEM DESCRIBED, PREVENT ONE SHIP TO HAVE IT'S IMAGE UPDATED
//                    if (starshipName != "Star Destroyer")  
                        self.updateArrayImage(index: index, image: unwrappedImageFromDocuments)
//                    
                    
                    completion(unwrappedImageFromDocuments)
                
                    // if image wasn't retrieved or doesn't exists, try to download from the internet
                else 
                    var image: UIImage?
                    self.downloadManager(imageUrl: unwrappedImageUrl)  data in
                        // if download was successful
                        if let unwrappedData = data 
                            // convert image data to image
                            image = UIImage(data: unwrappedData)
                            if let unwrappedImage = image 
                                self.updateArrayImage(index: index, image: unwrappedImage)
                                // save images locally on user documents folder so it can be used whenever it's needed
                                self.storeImage(image: unwrappedImage, imageName: starshipName)
                                completion(unwrappedImage)
                            
                        
                            // if download was not successful
                        else 
                            self.addImageNotFound(spinner: spinner, cell: cell)
                        
                    
                
            
        
            // if json has null on image_url value
        else 
            addImageNotFound(spinner: spinner, cell: cell)
        
    

如果需要,下面是我在 imageManager 上使用的一些辅助方法。

// MARK: - Helper Methods
    private func updateArrayImage(index: Int, image: UIImage) 
        // save image in the array so it can be used when cells are sorted
        self.starshipData[index].image = image
    
    
    private func downloadManager(imageUrl: URL, completion: @escaping (Data?) -> Void) 
        let session: URLSession = 
            let configuration = URLSessionConfiguration.default
            configuration.timeoutIntervalForRequest = 5
            return URLSession(configuration: configuration, delegate: nil, delegateQueue: nil)
        ()
        
        var dataTask: URLSessionDataTask?
        dataTask?.cancel()
        dataTask = session.dataTask(with: imageUrl)  [weak self] data, response, error in
            defer 
                dataTask = nil
            
            if let error = error 
                // use error if necessary
                DispatchQueue.main.async 
                    completion(nil)
                
            
            else if let response = response as? HTTPURLResponse,
                response.statusCode != 200 
                DispatchQueue.main.async 
                    completion(nil)
                
            
            else if let data = data,
                let response = response as? HTTPURLResponse,
                response.statusCode == 200  // Ok response
                DispatchQueue.main.async 
                    completion(data)
                
            
        
        dataTask?.resume()
    
    
    private func addImageNotFound(spinner: UIActivityIndicatorView, cell: StarshipCell) 
        spinner.stopAnimating()
        cell.starshipImgView.image = #imageLiteral(resourceName: "ImageNotFound")
    
    
    private func addImageToCell(cell: StarshipCell, spinner: UIActivityIndicatorView, image: UIImage) 
        DispatchQueue.main.async 
            spinner.stopAnimating()
            cell.starshipImgView.image = image
        
    
    
    private func imagePath(imageName: String) -> URL? 
        let fileManager = FileManager.default
        // path to save the images on documents directory
        guard let documentPath = fileManager.urls(for: .documentDirectory,
                                                  in: FileManager.SearchPathDomainMask.userDomainMask).first else  return nil 
        let appendedDocumentPath = documentPath.appendingPathComponent(imageName)
        return appendedDocumentPath
    
    
    private func retrieveImage(imageName: String) -> UIImage? 
        if let imagePath = self.imagePath(imageName: imageName),
            let imageData = FileManager.default.contents(atPath: imagePath.path),
            let image = UIImage(data: imageData) 
            return image
        
        return nil
    
    
    private func storeImage(image: UIImage, imageName: String) 
        if let jpgRepresentation = image.jpegData(compressionQuality: 1) 
            if let imagePath = self.imagePath(imageName: imageName) 
                do  
                    try jpgRepresentation.write(to: imagePath,
                                                options: .atomic)
                 catch let err 
                
            
        
    
    
    private func startSpinner(spinner: UIActivityIndicatorView, cell: StarshipCell) 
        spinner.center = cell.starshipImgView.center
        cell.starshipContentView.addSubview(spinner)
        spinner.startAnimating()
    
    

总而言之,这里是无序列表,当你打开应用程序时:unordered

按下排序按钮后的预期结果(大部分时间发生):ordered

错误的结果(很少发生),按下排序按钮后:error

如果需要,我很乐意添加更多信息,你!

【问题讨论】:

【参考方案1】:

首先,考虑移动 UITableViewCell 类的单元格配置。像这样:

class StarshipCell 

    private var starshipNameLabel = UILabel()
    private var starshipImgView = UIImageView()

    func configure(with model: Starship) 
       starshipNameLabel.text = model.name
       starshipImgView.downloadedFrom(link: model.imageUrl)
    

tableView(_:cellForRowAt:).中调用configure(with: Starship)方法

configure(with: Starship)内部调用的方法downloadedFrom(link: )由以下扩展提供

extension UIImageView 
    func downloadedFrom(url: URL, contentMode mode: UIView.ContentMode = .scaleAspectFit) 
        contentMode = mode
        URLSession.shared.dataTask(with: url)  data, response, error in
        guard let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
            let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
            let data = data, error == nil,
            let image = UIImage(data: data)
            else  return 
            DispatchQueue.main.async() 
                self.image = image
            
        .resume()
    

    func downloadedFrom(link: String?, contentMode mode: UIView.ContentMode = .scaleAspectFit) 
        if let link = link 
            guard let url = URL(string: link) else  return 
            downloadedFrom(url: url, contentMode: mode)
        
    

【讨论】:

感谢您提供的信息。我有个问题。在 cellForRowAt 方法中调用 configure 将使每个单元格都下载图像,即使它们没有显示。那会发生吗?我知道在这个例子中我只有 4 艘船,但我想知道如果有 1000 艘会发生什么。另一件事,你能告诉我我的例子做错了什么吗?我想知道使用 willDisplay 方法是否错误。 - 方法tableView(_:cellForRowAt:). 仅在单元格出现时调用。因此,如果您有 1000 个元素,则内存中没有 1000 个图像。 - 特别是我从未使用过 willDisplay 方法。我阅读了文档,但这是一个安静的困惑。我建议您不要将 willDisplay 用于此目的。我很抱歉不知道任何例子。最后,您可以阅读这篇文章medium.com/ios-seminar/…,其中阐明了表格视图的工作原理。如果这解决了您的问题,请将答案标记为解决方案。 嗨,朋友,我花了一些时间来测试一下。是的,您的示例似乎解决了问题,我将标记为解决方案。我也尝试将 willDisplay 代码移动到 cellForRowAt 方法,我在我的 json 中添加了更多对象,甚至尝试在其他特定位置添加/删除一些调度队列,正在进行大量测试。我仍在确保一切顺利,并且正在遵循一些有关如何操作的指南。感谢您的帮助!

以上是关于无法重新排序 tableView 单元格图像的主要内容,如果未能解决你的问题,请参考以下文章

重新加载单元格中没有图像的表格视图重新加载

使用 NSFetchedResultsController 核心数据重新排序 TableView 单元格 - Swift 3

从 tableView 中的 Firestore 单元格读取数据后随机重新排序

如何制作像 UITableView 一样的动画重新排序单元格?

如何在swift 4中重新排序单元格后保存tableView顺序

根据图像问题的 UITableview 单元格高度