Swift: asynchronously loading and displaying photo

2020-02-04 22:23发布

问题:

I'm struggling with the problem of displaying photo gallery from iPhone to collectionView.

Everything works fine if someone has 50 photos inside the gallery. The problem is when someone has thousands of photos, then the gallery is loading for 10 seconds, which is not good for my app.
The same problem occurs when I'm loading images from Facebook. The app is waits until it downloads every photo then it displays. I'd like to display images one by one during the loading operation instead of waiting for it to load it all.

I know that I should use DispachQueue and I did but there is some bug that I don't see.
Here is the code I use to fetch images from iPhone gallery:

func grapPhotos() {
    let imgManager = PHImageManager.default()
    let requestOptions = PHImageRequestOptions()
    requestOptions.isSynchronous = true
    requestOptions.deliveryMode = .highQualityFormat
    let fetchOptions = PHFetchOptions()

    fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]

    if let fetchResulat : PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions) {

        if fetchResulat.count > 0 {

            for i in 0..<fetchResulat.count {

                imgManager.requestImage(for: fetchResulat.object(at: i), targetSize: CGSize(width: 200, height:200), contentMode: PHImageContentMode.aspectFill, options: requestOptions, resultHandler: {
                    (image, eror) in

                    self.imageArray.append(image!)

                    DispatchQueue.main.async {
                        self.collectionView.reloadData()
                    }
                })
            }
        } else {

            print("You have no photos")
            self.collectionView.reloadData()
        }
    }
}

And the code to display them in collectionView:

 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return imageArray.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "galleryCell", for: indexPath) as! GalleryCollectionViewCell

        let image = self.imageArray[indexPath.row]

        DispatchQueue.main.async {
            cell.gelleryImages.image = image
            }

    return cell
}

Probably the same problem is with my Facebook class so I will be very grateful for your help.

回答1:

What you need is to add a fetchResult property to your collection view controller and fetch your image Assets inside viewDidLoad method.

var fetchResult: PHFetchResult<PHAsset> = PHFetchResult()

func fetchAssets() {
    let fetchOptions = PHFetchOptions()
    fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
    fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
}

override func viewDidLoad() {
    super.viewDidLoad()
    fetchAssets()
}

Next step is extend UIImageView to request the image data asynchronously setting its image on completion.

func fetchImage(asset: PHAsset, contentMode: PHImageContentMode, targetSize: CGSize) {
    let options = PHImageRequestOptions()
    options.version = .original
    PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, _ in
        guard let image = image else { return }
        switch contentMode {
        case .aspectFill:
            self.contentMode = .scaleAspectFill
        case .aspectFit:
            self.contentMode = .scaleAspectFit
        }
        self.image = image
    }
}

Now you can fetch the images one at a time inside collection view cellForItemAt method:

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! CollectionViewCell
    let asset = fetchResult.object(at: indexPath.row)
    cell.imageView.fetchImage(asset: asset, contentMode: .aspectFit, targetSize: cell.imageView.frame.size)
    return cell
}

Don't forget to return the fetchResult count for numberOfItemsInSection method.

override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return fetchResult.count
}

Sanple project



回答2:

Have you looked into SDWebImage? It has built in cache support so that the device doesn't have to download images it has previously downloaded every single time. It also has the benefit of being very simple to implement!