无法重新排序 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 一样的动画重新排序单元格?