当异步附加新项目时,UICollectionView快速滚动在单元格中显示错误的图像

时间:2017-05-19 18:50:49

标签: ios swift3 uicollectionview uicollectionviewcell

我有一个UICollectionView,它在网格中显示图像但是当我快速滚动时,它会暂时在单元格中显示错误的图像,直到从S3存储器下载图像,然后显示正确的图像。

我之前在SO上看到过与此问题有关的问题和答案,但没有一个解决方案适合我。在API调用之后填充字典let items = [[String: Any]]()。我需要细胞从废弃的细胞中丢弃图像。现在有一种令人不快的形象“跳舞”效果。

这是我的代码:

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 = $0.value as UIImage? else {
                return completionHandler(nil)
            }

            completionHandler(image)
        }
    }
}

如果用户滚动超出内容限制,我的收藏视图将加载更多项目。这似乎是细胞重用重用图像的问题的根源。单元格中的其他字段(例如项目标题)也会在加载新项目时进行交换。

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

4 个答案:

答案 0 :(得分:9)

当集合视图滚动单元格超出边界时,集合视图可以重复使用单元格来显示不同的项目。这就是您从名为dequeueReusableCell(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
}

如果集合视图不再显示任何单元格中的项目,则图像将被丢弃。如果集合视图在任何单元格中显示该项目(即使在不同的单元格中),您将将图像存储在正确的图像视图中。

答案 1 :(得分:3)

试试这个:

每当重用单元格时,都会调用其<Window x:Class="Sample4.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Sample4" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <local:PrimaryViewModel /> </Window.DataContext> <Grid> <DataGrid Name="usersData" ItemsSource="{Binding Source=_UsersList}" AutoGenerateColumns="False"> <DataGrid.Columns> <DataGridTextColumn Binding="{Binding Path=ID}" /> <DataGridTextColumn Binding="{Binding Path=UserName}" /> </DataGrid.Columns> </DataGrid> </Grid> </Window> 方法。你可以在这里重置你的手机。在您的情况下,您可以在此处的图像视图中设置默认图像,直到下载原始图像。

prepareForReuse

答案 2 :(得分:1)

@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都在请求时返回ImageTask,除非图像在缓存中。如果有,请保留对此ImageTask的引用。在prepareForReuse函数中。在其中调用ImageTask.cancel()并将imageTask设置为nil。

答案 3 :(得分:0)

来自Nuke项目页面:

  

Nuke.loadImage(with:into :)方法取消先前与目标关联的未完成请求。 Nuke持有对目标的弱引用,当目标被解除分配时,相关请求会自动取消。

据我所知,你正在正确使用他们的示例代码:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    ...
    cell.imageView.image = nil
    Nuke.loadImage(with: url, into: cell.imageView)
    ...
}

您明确地将UIImageViews图像设置为nil,因此在Nuke将新图像加载到目标中之前,实际上它无法显示图像。

此外,Nuke将取消之前的请求。我自己在使用Nuke并且没有这些问题。你确定你的“改变代码”不是这里的错误吗?对我来说,一切似乎都是正确的,我没有看到为什么它不应该工作的问题。

一个可能的问题可能是重新加载整个集合视图。我们应该始终尝试提供最佳的用户体验,因此如果您更改集合视图的数据源,请尝试使用performBatchUpdates为插入/更新/移动/删除设置动画。

一个很好的库可以自动处理这个,例如Dwifft。