UITableViewCell 异步加载图像问题 - Swift
UITableViewCell asynchronously loading images issue - Swift
在我的应用中,我构建了自己的异步图像加载 class。我传入一个对象,然后它检查缓存 (NSCache) 是否有图像,如果没有,它会检查文件系统是否已保存图像。如果图像尚未保存,它将在后台下载图像(NSOperations 帮助)。
到目前为止效果很好,但我 运行 遇到了 table 视图加载图像的一些小问题。
首先,这是我用来从tableView(tableView:, willDisplayCell:, forRowAtIndexPath:)
设置table视图单元格的函数
func configureCell(cell: ShowTableViewCell, indexPath: NSIndexPath) {
// Configure cell
if let show = dataSource.showFromIndexPath(indexPath) {
ImageManager.sharedManager.getImageForShow(show, completionHandler: { (image) -> Void in
if self.indexPathsForFadedInImages.indexOf(indexPath) == nil {
self.indexPathsForFadedInImages.append(indexPath)
if let fetchCell = self.tableView.cellForRowAtIndexPath(indexPath) as? ShowTableViewCell {
func fadeInImage() {
// Fade in image
fetchCell.backgroundImageView!.alpha = 0.0
fetchCell.backgroundImage = image
UIView.animateWithDuration(showImageAnimationSpeed, animations: { () -> Void in
fetchCell.backgroundImageView!.alpha = 1.0
})
}
if #available(iOS 9, *) {
if NSProcessInfo.processInfo().lowPowerModeEnabled {
fetchCell.backgroundImage = image
}
else {
fadeInImage()
}
}
else {
fadeInImage()
}
}
else {
// Issues are here
}
}
else {
// Set image
cell.backgroundImage = image
}
})
...
}
“// Issues are here”评论是我 运行 进入多个问题的地方。
到目前为止,我还没有想出另一种方法来验证图像是否属于单元格,以确定“// Issues are here”所在的位置。如果我添加
cell.backgroundImage = 图片
那里,然后它解决了有时图像不会显示在 table 视图单元格上的问题。到目前为止,我发现的唯一原因是图像 returned 的速度比 return table 视图单元格快,所以这就是 table 视图的原因表示该索引路径上没有单元格。
但是如果我在那里添加该代码,那么我 运行 会进入另一个问题!单元格会显示错误的图像,然后它会滞后应用程序,图像会不断切换,甚至只是停留在错误的图像上。
我检查过它在主线程上 运行s,图像下载和缓存都没有问题。它只需要做到 table 表示该索引路径上没有单元格,我已经尝试为 return 也为 nil 的单元格获取索引路径。
此问题的半解在 viewWillAppear/viewDidAppear 中称为 tableView.reloadData()。这将解决此问题,但随后我丢失了屏幕上 table 视图单元格的动画。
编辑:
如果我将图像视图传递给 getImageForShow() 并直接设置它,它将解决此问题,但那不是理想的代码设计。图像视图显然存在,单元格存在,但由于某种原因它不想每次都工作。
Table 视图重用单元格以节省内存,这可能会导致需要执行以显示单元格数据(如加载图像)的任何异步例程出现问题。如果单元格应该在异步操作完成时显示不同的数据,应用可能会突然进入不一致的显示状态。
为了解决这个问题,我建议向您的单元添加一代 属性,并在异步操作完成时检查 属性:
protocol MyImageManager {
static var sharedManager: MyImageManager { get }
func getImageForUrl(url: String, completion: (UIImage?, NSError?) -> Void)
}
struct MyCellData {
let url: String
}
class MyTableViewCell: UITableViewCell {
// The generation will tell us which iteration of the cell we're working with
var generation: Int = 0
override func prepareForReuse() {
super.prepareForReuse()
// Increment the generation when the cell is recycled
self.generation++
self.data = nil
}
var data: MyCellData? {
didSet {
// Reset the display state
self.imageView?.image = nil
self.imageView?.alpha = 0
if let data = self.data {
// Remember what generation the cell is on
var generation = self.generation
// In case the image retrieval takes a long time and the cell should be destroyed because the user navigates away, make a weak reference
weak var wcell = self
// Retrieve the image from the server (or from the local cache)
MyImageManager.sharedManager.getImageForUrl(data.url, completion: { (image, error) -> Void in
if let error = error {
println("There was a problem fetching the image")
} else if let cell = wcell, image = image where cell.generation == generation {
// Make sure that UI updates happen on main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// Only update the cell if the generation value matches what it was prior to fetching the image
cell.imageView?.image = image
cell.imageView?.alpha = 0
UIView.animateWithDuration(0.25, animations: { () -> Void in
cell.imageView?.alpha = 1
})
})
}
})
}
}
}
}
class MyTableViewController: UITableViewController {
var rows: [MyCellData] = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Identifier") as! MyTableViewCell
cell.data = self.rows[indexPath.row]
return cell
}
}
一些其他注意事项:
- 不要忘记在主线程上进行显示更新。在网络 activity 线程上更新可能会导致显示在看似随机的时间(或从不)发生变化
- 确保在执行异步操作时弱引用单元格(或任何其他 UI 元素),以防 UI 在异步操作完成之前被销毁。
在我的应用中,我构建了自己的异步图像加载 class。我传入一个对象,然后它检查缓存 (NSCache) 是否有图像,如果没有,它会检查文件系统是否已保存图像。如果图像尚未保存,它将在后台下载图像(NSOperations 帮助)。
到目前为止效果很好,但我 运行 遇到了 table 视图加载图像的一些小问题。
首先,这是我用来从tableView(tableView:, willDisplayCell:, forRowAtIndexPath:)
func configureCell(cell: ShowTableViewCell, indexPath: NSIndexPath) {
// Configure cell
if let show = dataSource.showFromIndexPath(indexPath) {
ImageManager.sharedManager.getImageForShow(show, completionHandler: { (image) -> Void in
if self.indexPathsForFadedInImages.indexOf(indexPath) == nil {
self.indexPathsForFadedInImages.append(indexPath)
if let fetchCell = self.tableView.cellForRowAtIndexPath(indexPath) as? ShowTableViewCell {
func fadeInImage() {
// Fade in image
fetchCell.backgroundImageView!.alpha = 0.0
fetchCell.backgroundImage = image
UIView.animateWithDuration(showImageAnimationSpeed, animations: { () -> Void in
fetchCell.backgroundImageView!.alpha = 1.0
})
}
if #available(iOS 9, *) {
if NSProcessInfo.processInfo().lowPowerModeEnabled {
fetchCell.backgroundImage = image
}
else {
fadeInImage()
}
}
else {
fadeInImage()
}
}
else {
// Issues are here
}
}
else {
// Set image
cell.backgroundImage = image
}
})
...
}
“// Issues are here”评论是我 运行 进入多个问题的地方。
到目前为止,我还没有想出另一种方法来验证图像是否属于单元格,以确定“// Issues are here”所在的位置。如果我添加
cell.backgroundImage = 图片
那里,然后它解决了有时图像不会显示在 table 视图单元格上的问题。到目前为止,我发现的唯一原因是图像 returned 的速度比 return table 视图单元格快,所以这就是 table 视图的原因表示该索引路径上没有单元格。
但是如果我在那里添加该代码,那么我 运行 会进入另一个问题!单元格会显示错误的图像,然后它会滞后应用程序,图像会不断切换,甚至只是停留在错误的图像上。
我检查过它在主线程上 运行s,图像下载和缓存都没有问题。它只需要做到 table 表示该索引路径上没有单元格,我已经尝试为 return 也为 nil 的单元格获取索引路径。
此问题的半解在 viewWillAppear/viewDidAppear 中称为 tableView.reloadData()。这将解决此问题,但随后我丢失了屏幕上 table 视图单元格的动画。
编辑:
如果我将图像视图传递给 getImageForShow() 并直接设置它,它将解决此问题,但那不是理想的代码设计。图像视图显然存在,单元格存在,但由于某种原因它不想每次都工作。
Table 视图重用单元格以节省内存,这可能会导致需要执行以显示单元格数据(如加载图像)的任何异步例程出现问题。如果单元格应该在异步操作完成时显示不同的数据,应用可能会突然进入不一致的显示状态。
为了解决这个问题,我建议向您的单元添加一代 属性,并在异步操作完成时检查 属性:
protocol MyImageManager {
static var sharedManager: MyImageManager { get }
func getImageForUrl(url: String, completion: (UIImage?, NSError?) -> Void)
}
struct MyCellData {
let url: String
}
class MyTableViewCell: UITableViewCell {
// The generation will tell us which iteration of the cell we're working with
var generation: Int = 0
override func prepareForReuse() {
super.prepareForReuse()
// Increment the generation when the cell is recycled
self.generation++
self.data = nil
}
var data: MyCellData? {
didSet {
// Reset the display state
self.imageView?.image = nil
self.imageView?.alpha = 0
if let data = self.data {
// Remember what generation the cell is on
var generation = self.generation
// In case the image retrieval takes a long time and the cell should be destroyed because the user navigates away, make a weak reference
weak var wcell = self
// Retrieve the image from the server (or from the local cache)
MyImageManager.sharedManager.getImageForUrl(data.url, completion: { (image, error) -> Void in
if let error = error {
println("There was a problem fetching the image")
} else if let cell = wcell, image = image where cell.generation == generation {
// Make sure that UI updates happen on main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// Only update the cell if the generation value matches what it was prior to fetching the image
cell.imageView?.image = image
cell.imageView?.alpha = 0
UIView.animateWithDuration(0.25, animations: { () -> Void in
cell.imageView?.alpha = 1
})
})
}
})
}
}
}
}
class MyTableViewController: UITableViewController {
var rows: [MyCellData] = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Identifier") as! MyTableViewCell
cell.data = self.rows[indexPath.row]
return cell
}
}
一些其他注意事项:
- 不要忘记在主线程上进行显示更新。在网络 activity 线程上更新可能会导致显示在看似随机的时间(或从不)发生变化
- 确保在执行异步操作时弱引用单元格(或任何其他 UI 元素),以防 UI 在异步操作完成之前被销毁。