当异步附加新项目时,UICollectionView 快速滚动在单元格中显示错误的图像
UICollectionView fast scrolling displays wrong images in the cell when new items are appended asynchronously
我有一个 UICollectionView,它在网格中显示图像,但是当我快速滚动时,它会暂时在单元格中显示错误的图像,直到从 S3 存储下载图像,然后显示正确的图像。
我之前在 SO 上看到过与此问题相关的问题和答案,但 none 的解决方案对我有用。字典 let items = [[String: Any]]()
在 API 调用后被填充。我需要单元格从回收单元格中丢弃图像。现在有一个令人不快的图像 "dancing" 效果。
这是我的代码:
var searchResults = [[String: Any]]()
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MyCollectionViewCell
let item = searchResults[indexPath.item]
cell.backgroundColor = .white
cell.itemLabel.text = item["title"] as? String
let imageUrl = item["img_url"] as! String
let url = URL(string: imageUrl)
let request = Request(url: url!)
cell.itemImageView.image = nil
Nuke.loadImage(with: request, into: cell.itemImageView)
return cell
}
class MyCollectionViewCell: UICollectionViewCell {
var itemImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 0
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .white
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
---------
}
override func prepareForReuse() {
super.prepareForReuse()
itemImageView.image = nil
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
我将我的单元格图像加载更改为以下代码:
cell.itemImageView.image = nil
APIManager.sharedInstance.myImageQuery(url: imageUrl) { (image) in
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCollectionViewCell
else { return }
cell.itemImageView.image = image
cell.activityIndicator.stopAnimating()
}
这是我的 API 经理。
struct APIManager {
static let sharedInstance = APIManager()
func myImageQuery(url: String, completionHandler: @escaping (UIImage?) -> ()) {
if let url = URL(string: url) {
Manager.shared.loadImage(with: url, token: nil) { // internal to Nuke
guard let image = [=12=].value as UIImage? else {
return completionHandler(nil)
}
completionHandler(image)
}
}
}
如果用户滚动超过内容限制,我的 collection 视图将加载更多项目。这似乎是单元格重用重用图像的问题的根源。加载新项目时,单元格中的其他字段(例如项目标题)也会交换。
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.contentOffset.y + view.frame.size.height) > (scrollView.contentSize.height * 0.8) {
loadItems()
}
}
这是我的数据加载函数
func loadItems() {
if loadingMoreItems {
return
}
if noMoreData {
return
}
if(!Utility.isConnectedToNetwork()){
return
}
loadingMoreItems = true
currentPage = currentPage + 1
LoadingOverlay.shared.showOverlay(view: self.view)
APIManager.sharedInstance.getItems(itemURL, page: currentPage) { (result, error) -> () in
if error != nil {
}
else {
self.parseData(jsonData: result!)
}
self.loadingMoreItems = false
LoadingOverlay.shared.hideOverlayView()
}
}
func parseData(jsonData: [String: Any]) {
guard let items = jsonData["items"] as? [[String: Any]] else {
return
}
if items.count == 0 {
noMoreData = true
return
}
for item in items {
searchResults.append(item)
}
for index in 0..<self.searchResults.count {
let url = URL(string: self.searchResults[index]["img_url"] as! String)
let request = Request(url: url!)
self.preHeatedImages.append(request)
}
self.preheater.startPreheating(with: self.preHeatedImages)
DispatchQueue.main.async {
// appendCollectionView(numberOfItems: items.count)
self.collectionView.reloadData()
self.collectionView.collectionViewLayout.invalidateLayout()
}
}
当集合视图将单元格滚动到越界时,集合视图可能会重新使用该单元格来显示不同的项目。这就是为什么您从名为 dequeue<b>Reusable</b>Cell(withReuseIdentifier:for:)
.
的方法中获取单元格的原因
您需要确保图像准备就绪后,图像视图仍应显示该项目的图像。
我建议您更改 Nuke.loadImage(with:into:)
方法以获取闭包而不是图像视图:
struct Nuke {
static func loadImage(with request: URLRequest, body: @escaping (UIImage) -> ()) {
// ...
}
}
这样,在 collectionView(_:cellForItemAt:)
中,您可以像这样加载图像:
Nuke.loadImage(with: request) { [weak collectionView] (image) in
guard let cell = collectionView?.cellForItem(at: indexPath) as? MyCollectionViewCell
else { return }
cell.itemImageView.image = image
}
如果集合视图不再显示任何单元格中的项目,图像将被丢弃。如果集合视图在任何单元格中显示项目(即使在不同的单元格中),您将图像存储在正确的图像视图中。
试试这个:
每当重复使用单元格时,都会调用其 prepareForReuse
方法。您可以在此处重置您的手机。在您的情况下,您可以在此处的图像视图中设置默认图像,直到下载原始图像。
override func prepareForReuse()
{
super.prepareForReuse()
self.imageView.image = UIImage(named: "DefaultImage"
}
来自 Nuke 项目页面:
Nuke.loadImage(with:into:) method cancels previous outstanding request associated with the target. Nuke holds a weak reference to a target, when the target is deallocated the associated request gets cancelled automatically.
据我所知,您正在正确使用他们的示例代码:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
cell.imageView.image = nil
Nuke.loadImage(with: url, into: cell.imageView)
...
}
您明确将 UIImageViews 图像设置为 nil,因此在 Nuke 将新图像加载到目标之前,它实际上应该无法显示图像。
Nuke 还会取消之前的请求。我自己正在使用 Nuke,没有这些问题。您确定您的 "changed code" 不是这里的错误部分吗?对我来说,一切似乎都是正确的,我看不出为什么它不起作用。
一个可能的问题是重新加载整个集合视图。我们应该始终尝试提供最佳用户体验,因此如果您更改集合视图的数据源,请尝试使用 performBatchUpdates 为 inserts/updates/moves/deletes.
设置动画
一个很好的库可以自动处理这个问题,例如 Dwifft。
@discardableResult
public func loadImage(with url: URL,
options: ImageLoadingOptions = ImageLoadingOptions.shared,
into view: ImageDisplayingView,
progress: ImageTask.ProgressHandler? = nil,
completion: ImageTask.Completion? = nil) -> ImageTask? {
return loadImage(with: ImageRequest(url: url), options: options, into: view, progress: progress, completion: completion)
}
所有 Nuke API 的 return 在请求时都是一个 ImageTask,除非图像在缓存中。如果有,请保留对此 ImageTask 的引用。在 prepareForReuse 函数中。在其中调用 ImageTask.cancel() 并将 imageTask 设置为 nil。
我有一个 UICollectionView,它在网格中显示图像,但是当我快速滚动时,它会暂时在单元格中显示错误的图像,直到从 S3 存储下载图像,然后显示正确的图像。
我之前在 SO 上看到过与此问题相关的问题和答案,但 none 的解决方案对我有用。字典 let items = [[String: Any]]()
在 API 调用后被填充。我需要单元格从回收单元格中丢弃图像。现在有一个令人不快的图像 "dancing" 效果。
这是我的代码:
var searchResults = [[String: Any]]()
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MyCollectionViewCell
let item = searchResults[indexPath.item]
cell.backgroundColor = .white
cell.itemLabel.text = item["title"] as? String
let imageUrl = item["img_url"] as! String
let url = URL(string: imageUrl)
let request = Request(url: url!)
cell.itemImageView.image = nil
Nuke.loadImage(with: request, into: cell.itemImageView)
return cell
}
class MyCollectionViewCell: UICollectionViewCell {
var itemImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 0
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .white
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
---------
}
override func prepareForReuse() {
super.prepareForReuse()
itemImageView.image = nil
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
我将我的单元格图像加载更改为以下代码:
cell.itemImageView.image = nil
APIManager.sharedInstance.myImageQuery(url: imageUrl) { (image) in
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCollectionViewCell
else { return }
cell.itemImageView.image = image
cell.activityIndicator.stopAnimating()
}
这是我的 API 经理。
struct APIManager {
static let sharedInstance = APIManager()
func myImageQuery(url: String, completionHandler: @escaping (UIImage?) -> ()) {
if let url = URL(string: url) {
Manager.shared.loadImage(with: url, token: nil) { // internal to Nuke
guard let image = [=12=].value as UIImage? else {
return completionHandler(nil)
}
completionHandler(image)
}
}
}
如果用户滚动超过内容限制,我的 collection 视图将加载更多项目。这似乎是单元格重用重用图像的问题的根源。加载新项目时,单元格中的其他字段(例如项目标题)也会交换。
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.contentOffset.y + view.frame.size.height) > (scrollView.contentSize.height * 0.8) {
loadItems()
}
}
这是我的数据加载函数
func loadItems() {
if loadingMoreItems {
return
}
if noMoreData {
return
}
if(!Utility.isConnectedToNetwork()){
return
}
loadingMoreItems = true
currentPage = currentPage + 1
LoadingOverlay.shared.showOverlay(view: self.view)
APIManager.sharedInstance.getItems(itemURL, page: currentPage) { (result, error) -> () in
if error != nil {
}
else {
self.parseData(jsonData: result!)
}
self.loadingMoreItems = false
LoadingOverlay.shared.hideOverlayView()
}
}
func parseData(jsonData: [String: Any]) {
guard let items = jsonData["items"] as? [[String: Any]] else {
return
}
if items.count == 0 {
noMoreData = true
return
}
for item in items {
searchResults.append(item)
}
for index in 0..<self.searchResults.count {
let url = URL(string: self.searchResults[index]["img_url"] as! String)
let request = Request(url: url!)
self.preHeatedImages.append(request)
}
self.preheater.startPreheating(with: self.preHeatedImages)
DispatchQueue.main.async {
// appendCollectionView(numberOfItems: items.count)
self.collectionView.reloadData()
self.collectionView.collectionViewLayout.invalidateLayout()
}
}
当集合视图将单元格滚动到越界时,集合视图可能会重新使用该单元格来显示不同的项目。这就是为什么您从名为 dequeue<b>Reusable</b>Cell(withReuseIdentifier:for:)
.
您需要确保图像准备就绪后,图像视图仍应显示该项目的图像。
我建议您更改 Nuke.loadImage(with:into:)
方法以获取闭包而不是图像视图:
struct Nuke {
static func loadImage(with request: URLRequest, body: @escaping (UIImage) -> ()) {
// ...
}
}
这样,在 collectionView(_:cellForItemAt:)
中,您可以像这样加载图像:
Nuke.loadImage(with: request) { [weak collectionView] (image) in
guard let cell = collectionView?.cellForItem(at: indexPath) as? MyCollectionViewCell
else { return }
cell.itemImageView.image = image
}
如果集合视图不再显示任何单元格中的项目,图像将被丢弃。如果集合视图在任何单元格中显示项目(即使在不同的单元格中),您将图像存储在正确的图像视图中。
试试这个:
每当重复使用单元格时,都会调用其 prepareForReuse
方法。您可以在此处重置您的手机。在您的情况下,您可以在此处的图像视图中设置默认图像,直到下载原始图像。
override func prepareForReuse()
{
super.prepareForReuse()
self.imageView.image = UIImage(named: "DefaultImage"
}
来自 Nuke 项目页面:
Nuke.loadImage(with:into:) method cancels previous outstanding request associated with the target. Nuke holds a weak reference to a target, when the target is deallocated the associated request gets cancelled automatically.
据我所知,您正在正确使用他们的示例代码:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
cell.imageView.image = nil
Nuke.loadImage(with: url, into: cell.imageView)
...
}
您明确将 UIImageViews 图像设置为 nil,因此在 Nuke 将新图像加载到目标之前,它实际上应该无法显示图像。
Nuke 还会取消之前的请求。我自己正在使用 Nuke,没有这些问题。您确定您的 "changed code" 不是这里的错误部分吗?对我来说,一切似乎都是正确的,我看不出为什么它不起作用。
一个可能的问题是重新加载整个集合视图。我们应该始终尝试提供最佳用户体验,因此如果您更改集合视图的数据源,请尝试使用 performBatchUpdates 为 inserts/updates/moves/deletes.
设置动画一个很好的库可以自动处理这个问题,例如 Dwifft。
@discardableResult
public func loadImage(with url: URL,
options: ImageLoadingOptions = ImageLoadingOptions.shared,
into view: ImageDisplayingView,
progress: ImageTask.ProgressHandler? = nil,
completion: ImageTask.Completion? = nil) -> ImageTask? {
return loadImage(with: ImageRequest(url: url), options: options, into: view, progress: progress, completion: completion)
}
所有 Nuke API 的 return 在请求时都是一个 ImageTask,除非图像在缓存中。如果有,请保留对此 ImageTask 的引用。在 prepareForReuse 函数中。在其中调用 ImageTask.cancel() 并将 imageTask 设置为 nil。