4

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.

mert dökümcü
  • 761
  • 3
  • 9
  • 31
Matt199
  • 264
  • 2
  • 18
  • what exactly is not working? – Ruslan Serebriakov Jan 06 '18 at 15:52
  • Using this code, the app is waiting couple of seconds until it downloads all the images, but I want to display the images one by one while 'downloading' without waiting. When one image is ready then display it etc until all are displayed – Matt199 Jan 06 '18 at 15:57
  • @Matt199 If you are displaying them I'm a collection view it doesn't make sense to download high quality format. Change your deliveryMode to `.fastformat`. https://developer.apple.com/documentation/photos/phimagerequestoptionsdeliverymode/1616955-fastformat and if needed download only one picture at a time when displaying it individually at high quality. – Leo Dabus Jan 06 '18 at 17:20
  • @LeoDabus yeah, I actually changed it to `.fastFormat` but it still takes some time to load. It would be awesome if it could display one by one image. Not all together but one by one when its ready – Matt199 Jan 06 '18 at 17:25
  • You would need to fetchAssets and fetch one image at a time instead of the whole fetchAssets result in a loop – Leo Dabus Jan 06 '18 at 17:27
  • Yes but when I change it to false then the resolution is horrible.. – Matt199 Jan 06 '18 at 17:35
  • This is exactly what I need! Leo, Thank you very much. But one question, you are fetching one asset at a time inside `requestImageData` right? – Matt199 Jan 06 '18 at 18:31
  • It is requesting one at a time when the collection view call cellForRow method and it gets the result asynchronously – Leo Dabus Jan 06 '18 at 18:48
  • This is perfect! Thank you! you can also post an answer with the UIImageView extension;) cheers! – Matt199 Jan 06 '18 at 19:00

2 Answers2

6

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.

extension UIImageView {    
    func fetchImage(asset: PHAsset, contentMode: PHImageContentMode, targetSize: CGSize, version:  PHImageRequestOptionsVersion = .current, deliveryMode: PHImageRequestOptionsDeliveryMode = .opportunistic) {
        let options = PHImageRequestOptions()
        options.version = version
        options.deliveryMode = deliveryMode
        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
            @unknown default: fatalError()
            }
            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: .aspectFill, targetSize: cell.imageView.frame.size * UIScreen.main.scale )
    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
}

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

extension CGSize {
    static func *(lhs: CGSize, rhs: CGFloat) -> CGSize {
        .init(width: lhs.width * rhs, height: lhs.height * rhs)
    }
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Hi, @LeoDabus once again thanks for your help but there is one problem while using `spectFill` content mode, it gives me very low resolution of the photo and I can't change it.. – Matt199 Jan 07 '18 at 10:29
  • Also I'd like to display 3 photos in row, but then during scrolling the photos loses on the resolution. Strange – Matt199 Jan 07 '18 at 11:44
0

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!

Sean Ray
  • 322
  • 4
  • 12
  • No actually I have not but is it going to work also with iPhone gallery? – Matt199 Jan 06 '18 at 16:03
  • the issue with using SDWebImage is that it does not fetch the edited photos... as in any edit to the image or video will not be fetched... only the original base image. So, any crop or special effect (eg depth effect) won't be fetched... –  Jul 06 '20 at 01:51