异步下载图像时,“集合”视图单元格中的图像未更新

时间:2019-09-17 09:04:08

标签: ios swift

从服务器下载图像时,集合视图单元格中的图像未更新。滚动集合视图时,图像将更新。

表视图的每个部分都有一个集合视图。表格视图单元格具有用于集合视图的数据源。

extension OffersCell: UICollectionViewDataSource,UICollectionViewDelegate{
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

        return photoViewModel.photos.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath)
        (cell as! PhotoCell).imageView.contentMode = .scaleAspectFill
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {

        let photo = self.photoViewModel.photos[indexPath.row]
        (cell as! PhotoCell).imageView.image = UIImage(named: "dummyImage")

        ImageDownloadManager.shared.downloadImage(photo, indexPath: indexPath) { (image, imageIndexPath, error) in
            if let indexPathNew = imageIndexPath, indexPathNew == indexPath {
                DispatchQueue.main.async {
                    (cell as! PhotoCell).imageView.image = image
                }
            }
        }
    }

}

请找到图像下载器类:

typealias ImageDownloadHandler = (_ image: UIImage?, _ indexPath: IndexPath?, _ error: Error?) -> Void

final class ImageDownloadManager {

    private var completionHandler: ImageDownloadHandler?
    lazy var imageDownloadQueue: OperationQueue = {
        var queue = OperationQueue()
        queue.name = "imageDownloadQueue"
        queue.qualityOfService = .userInteractive
        return queue
    }()

    let imageCache = NSCache<NSString, UIImage>()
    static let shared = ImageDownloadManager()
    private init () {}

    func downloadImage(_ photo: Photos, indexPath: IndexPath?, handler: @escaping ImageDownloadHandler) {
        self.completionHandler = handler
        guard let url = photo.getImageURL() else {
            return
        }
        if let cachedImage = imageCache.object(forKey: photo.id as NSString) {
            self.completionHandler?(cachedImage, indexPath, nil)
        } else {
                let operation = CustomOperation(url: url, indexPath: indexPath)
                if indexPath == nil {
                }
            operation.queuePriority = .high
                operation.downloadHandler = { (image, indexPath, error) in
                    if let newImage = image {
                        self.imageCache.setObject(newImage, forKey: photo.id as NSString)
                    }
                    self.completionHandler?(image, indexPath, error)
                }
                imageDownloadQueue.addOperation(operation)
        }
    }

    func cancelAll() {
        imageDownloadQueue.cancelAllOperations()
    }


}

3 个答案:

答案 0 :(得分:3)

下载图像后,在主线程上执行指令(cell as! PhotoCell).imageView.image = image。但这不会重新显示您的collectionView单元。
同样,collectionView:willDisplayCell:forItemAtIndexPath:通常不会被调用。 docs

  

在将单元格添加到集合视图之前,集合视图会调用此方法   内容。

但是,当您在单元格中滚动时(即当它可见时),它被称为。这就是为什么在滚动单元格后显示图像的原因。

所以我的建议是:

  • 下载图像后,更新您的collectionView数据源 这样collectionView:cellForItemAtIndexPath:可以配置单元格 与图像。
  • 使用仅包含已更新单元格的索引路径的数组调用reloadItems(at:)

答案 1 :(得分:1)

这取决于您如何定义类CustomOperation,但是问题似乎出在downloadImage的方法ImageDownloadManager中,您在下一行中设置了self.completionHandler = handler。请注意,ImageDownloadManager是单例。这意味着您开始的每个操作都用新的完成替换单例对象的completionHandler(我敢打赌,只有最后一个单元格被刷新)。解决方案包括消除属性completionHandler并用此替换操作下载处理程序

operation.downloadHandler = { (image, indexPath, error) in
    if let newImage = image {
        self.imageCache.setObject(newImage, forKey: photo.id as NSString)
    }
    handler(image, indexPath, error)
}

请注意,它调用上下文的处理程序,而不是下载管理器的存储属性

这是一个包含所有类和结构定义的完整示例。根据需要进行调整。


typealias ImageDownloadHandler = (_ image: UIImage?, _ indexPath: IndexPath?, _ error: Error?) -> Void

enum ImageDownloadError: Error {
    case badDataURL
}

class CustomOperation: Operation {

    var downloadHandler: (UIImage?, IndexPath?, Error?) -> () = { _,_,_ in }

    private let url: URL
    private let indexPath: IndexPath?

    init(url: URL, indexPath: IndexPath?) {
        self.url = url
        self.indexPath = indexPath
    }

    override func main() {
        guard let imageData = try? Data(contentsOf: self.url) else {
            self.downloadHandler(nil, self.indexPath, ImageDownloadError.badDataURL)
            return
        }
        let image = UIImage(data: imageData)
        self.downloadHandler(image, self.indexPath, nil)
    }
}

final class ImageDownloadManager {

    private var completionHandler: ImageDownloadHandler?
    lazy var imageDownloadQueue: OperationQueue = {
        var queue = OperationQueue()
        queue.name = "imageDownloadQueue"
        queue.qualityOfService = .userInteractive
        return queue
    }()

    let imageCache = NSCache<NSString, UIImage>()
    static let shared = ImageDownloadManager()
    private init () {}

    func downloadImage(_ photo: Photos, indexPath: IndexPath?, handler: @escaping ImageDownloadHandler) {
        //self.completionHandler = handler
        guard let url = photo.getImageURL() else {
            return
        }
        if let cachedImage = imageCache.object(forKey: photo.id as NSString) {
            //self.completionHandler?(cachedImage, indexPath, nil)
            handler(cachedImage, indexPath, nil)
        } else {
            let operation = CustomOperation(url: url, indexPath: indexPath)
            if indexPath == nil {
            }
            operation.queuePriority = .high
            operation.downloadHandler = { (image, indexPath, error) in
                if let newImage = image {
                    self.imageCache.setObject(newImage, forKey: photo.id as NSString)
                }
                //self.completionHandler?(image, indexPath, error)
                handler(image, indexPath, error)
            }
            imageDownloadQueue.addOperation(operation)
        }
    }

    func cancelAll() {
        imageDownloadQueue.cancelAllOperations()
    }
}

-------------------------------------------------------

struct Photos {
    let id: String
    let url: URL

    func getImageURL() -> URL? {
        return self.url
    }
}

struct PhotoViewModel {
    let photos: [Photos]
}

class PhotoCell: UICollectionViewCell {
    @IBOutlet weak var imageView: UIImageView!
}

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!

    private let photoViewModel: PhotoViewModel = PhotoViewModel(
        photos: [
            Photos(
                id: "kitty1",
                url: URL(
                    string: "https://cdn.pixabay.com/photo/2019/06/18/11/23/cat-4282110_960_720.jpg"
                )!
            ),
            Photos(
                id: "kitty2",
                url: URL(
                    string: "https://cdn.pixabay.com/photo/2019/07/23/20/08/cat-4358536_960_720.jpg"
                    )!
            ),
            Photos(
                id: "kitty3",
                url: URL(
                    string: "https://cdn.pixabay.com/photo/2016/09/28/13/15/kittens-1700474_960_720.jpg"
                    )!
            )
        ]
    )

    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.reloadData()
    }
}


extension ViewController: UICollectionViewDataSource,UICollectionViewDelegate{
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return photoViewModel.photos.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath)
        (cell as! PhotoCell).imageView.contentMode = .scaleAspectFill
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {

        let photo = self.photoViewModel.photos[indexPath.row]
        (cell as! PhotoCell).imageView.image = UIImage(named: "dummyImage")

        ImageDownloadManager.shared.downloadImage(photo, indexPath: indexPath) { (image, imageIndexPath, error) in
            if let indexPathNew = imageIndexPath, indexPathNew == indexPath {
                DispatchQueue.main.async {
                    (cell as! PhotoCell).imageView.image = image
                }
            }
        }
    }

}

答案 2 :(得分:0)

是的,除非按照@ReinhardMänner的说明滚动收藏集视图,否则将不会显示下载的图片

相反,您可以使用适合您需求的第三方SDK来在应用程序中进行图像下载和缓存。

我建议您使用Kingfisher SDK(以迅捷方式开发)。

  

易于使用和集成。它做很多事情,例如异步。下载,缓存(在内存或磁盘上),设置图像时的内置过渡动画等,并且也很受欢迎

对于您的问题,如果您使用Kingfisher SDK,则只有一行代码。

例如。

要异步加载图像,可以在cellForRowAtItem:方法中使用以下方法。

let url = URL(string: "https://example.com/image.png")
imageView.kf.setImage(with: url)

你们所有人需要做的是...

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as! PhotoCell
    cell.imageView.contentMode = .scaleAspectFill

    //I'm assuming photo is URL(in string) of Photo. if 'photo' is URL type then you can pass it directly in 'setImage' method.
    let photo = self.photoViewModel.photos[indexPath.row]

    let imgUrl = URL(string: photo)

    //It will download image asynchronously and cache it for later use. If the image is failed to downloaded due to some issue then "dummyImage" will be set in image view.
    cell.imageView.kf.setImage(with: imgUrl, placeholder: UIImage(named: "dummyImage"))

    return cell
}
  

在这里您可以删除单元格willDisplay:方法。