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

Failling to reorder tableView cell images

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

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

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

这是我的排序方法,很基础。

@objc private func sortByCost(sender: UIBarButtonItem) {
        starshipData.sort { [=10=].costInCredits < .costInCredits }
        starshipTableView.reloadData()
}

下面是 tableView 的实现。

首先,我使用 cellForRowAt 方法填充 fast/light 数据。

    // 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 的了解,我认为问题出在这里,后台线程仍在开发中。

我通过打印日志发现单元格未显示正确图像的次数是因为数组没有该索引的图像,因此单元格显示上次 table 是 populated/loaded.

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

问题是,如果 table 第一次被正确填充,当按下排序按钮时,starshipData 数组应该已经包含所有图像,正如您在 imageManager 方法中看到的那样,在image 是 unwrappedFromDocuments,我调用 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()
    }
    
}

总而言之,这是未排序的 table,当您打开应用程序时:unordered

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

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

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

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

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