PHImageManager requestImageForAsset returns nil sometimes for iCloud photos
Asked Answered
M

11

59

Roughly 10% of the time PHImageManager.defaultManager().requestImageForAsset returns nil instead of a valid UIImage after first returning a valid though "degraded" UIImage. No error or other clue that I can see is returned in the info with the nil.

This seems to happen with photos that need to be downloaded from iCloud, with iCloud Photo Library and Optimize iPad Storage both enabled. I've tried changing the options, size, etc. but nothing seems to matter.

If I retry the requestImageForAsset after the failure it will usually correctly return a UIImage, though sometimes it requires a couple of retries.

Any idea what I might be doing wrong? Or is it just a bug in the Photos framework?

    func photoImage(asset: PHAsset, size: CGSize, contentMode: UIViewContentMode, completionBlock:(image: UIImage, isPlaceholder: Bool) -> Void) -> PHImageRequestID? {

    let options = PHImageRequestOptions()
    options.networkAccessAllowed = true
    options.version = .Current
    options.deliveryMode = .Opportunistic
    options.resizeMode = .Fast

    let requestSize = !CGSizeEqualToSize(size, CGSizeZero) ? size : PHImageManagerMaximumSize
    let requestContentMode = contentMode == .ScaleAspectFit ? PHImageContentMode.AspectFit : PHImageContentMode.AspectFill

    return PHImageManager.defaultManager().requestImageForAsset(asset, targetSize: requestSize, contentMode: requestContentMode, options: options)
        { (image: UIImage!, info: [NSObject : AnyObject]!) in
            if let image = image {
                let degraded = info[PHImageResultIsDegradedKey] as? Bool ?? false
                completionBlock(image: photoBlock.rotatedImage(image), isPlaceholder: degraded)

            } else {
                let error = info[PHImageErrorKey] as? NSError
                NSLog("Nil image error = \(error?.localizedDescription)")
           }
    }
}
Maladjustment answered 24/6, 2015 at 21:58 Comment(6)
I have exactly the same problem, did you find any solution for this?Christenson
I am seeing the same issue. I believe it is a bug and have filed a bug report.Surrebuttal
I am seeing this happen when I set options.deliveryMode = .HighQualityFormat but not when I set options.deliveryMode = .OpportunisticBiotin
@AlfieHanssen sadly makes no difference for meDrucill
Can anyone tell me the solution of this #55489498? I am also facing the same issue with Videos.Finnie
@Christenson , jordan H , Lenk are you got any solution for this issue?Cryptograph
D
54

I just went through this too. By my tests the issue appears on devices that have the "Optimize Storage" option enabled and resides in the difference between the two methods bellow:

[[PHImageManager defaultManager] requestImageForAsset: ...]

This will successfully fetch remote iCloud images if your options are correctly configured.


[[PHImageManager defaultManager] requestImageDataForAsset:...]

This function only works for images that reside on the phones memory or that were recently fetched from iCloud by your app on any other one.


Here's a working snippet I'm using -bear with me the Obj-c :)

 PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
 options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; //I only want the highest possible quality
 options.synchronous = NO;
 options.networkAccessAllowed = YES;
 options.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) {
        NSLog(@"%f", progress); //follow progress + update progress bar
    };

  [[PHImageManager defaultManager] requestImageForAsset:myPhAsset targetSize:self.view.frame.size contentMode:PHImageContentModeAspectFill options:options resultHandler:^(UIImage *image, NSDictionary *info) {
        NSLog(@"reponse %@", info);
        NSLog(@"got image %f %f", image.size.width, image.size.height);
    }];

Full gist available on github

Updated for Swift 4:

    let options = PHImageRequestOptions()
    options.deliveryMode = PHImageRequestOptionsDeliveryMode.highQualityFormat
    options.isSynchronous = false
    options.isNetworkAccessAllowed = true

    options.progressHandler = {  (progress, error, stop, info) in
        print("progress: \(progress)")
    }

    PHImageManager.default().requestImage(for: myPHAsset, targetSize: view.frame.size, contentMode: PHImageContentMode.aspectFill, options: options, resultHandler: {
     (image, info) in
        print("dict: \(String(describing: info))")
        print("image size: \(String(describing: image?.size))")
    })
Delaine answered 3/8, 2015 at 10:23 Comment(12)
I don't really understand how this is supposed to solve the original problem?Christenson
I agree with dlinsin, this looks the same as what I'm doing and it still fails regularly.Maladjustment
I've been using it and never had a fail. It might also be a bug in the framework since it's still unstable.Delaine
The really problem with this method is how do you obtain the PHAsset from iCloud Drive elements... and suppose all you want is a thumbnail. Do you really have to download the entire image just to get the thumbnail?Thrombophlebitis
Also, this method requires PHAssets. I am not sure this will work for pictures on iCloud Drive. Probably not.Thrombophlebitis
This is not for iCloud Drive It's for fetching photos from iCloud Photo LibraryDelaine
The tricky part is networkAccessAllowed. From doc: "if necessary will download the image from iCloud". And by default it's set to false.Brewis
@JamesChen This is what I have found also - I get a crash without that set to true. Thank you ahbou!Theressa
GREAT HELP! I was having same problem. After I read this answer, i checked the device settings and Storage Optimize was enable for photos, I changed it to Download and keep Originals, and finally It works good now, Thanks @DelaineInfertile
Can anyone tell me the solution of this #55489498? I am also facing the same issue with Videos.Finnie
i am facing same issue when i get .gif and .video file.Cryptograph
Hey ahbou you helped me the most with a similar issue. As soon as I used the "isSynchronous = false" option everything worked as expected! I appreciated :)Denna
L
9

Nothing of the above worked for me, but this solution did!

private func getUIImage(asset: PHAsset, retryAttempts: Int = 10) -> UIImage? {
    var img: UIImage?
    let manager = PHImageManager.default()
    let options = PHImageRequestOptions()
    options.version = .original
    options.isSynchronous = true
    options.isNetworkAccessAllowed = true
    manager.requestImage(for: asset, targetSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight), contentMode: .aspectFit, options: options, resultHandler: { image, _ in
        img = image
    })
    if img == nil && retryAttempts > 0 {
        return getUIImage(asset: asset, retryAttempts: retryAttempts - 1)
    }
    return img
}

The difference here is the recursive retries. This works for me 100% of the times.

Luciana answered 10/3, 2020 at 10:38 Comment(2)
options.isNetworkAccessAllowed = true helped! thank you! =)Bushwhacker
options.isNetworkAccessAllowed = true solved my problems too.Woodchopper
P
7

I found that this had nothing to do with network or iCloud. It occasionally failed, even on images that were completely local. Sometimes it was images from my camera, sometimes it would be from images saved from the web.

I didn't find a fix, but a work around inspired by @Nadzeya that worked 100% of the time for me was to always request a target size equal to the asset size.

Eg.

PHCachingImageManager().requestImage(for: asset, 
                              targetSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight) , 
                             contentMode: .aspectFit, 
                                 options: options, 
                           resultHandler: { (image, info) in
        if (image == nil) {
            print("Error loading image")
            print("\(info)")
        } else {
            view.image = image
        }
    });

I believe the drawbacks to this would be that we're getting the full image back in memory, and then forcing the ImageView to do the scaling, but at least in my use case, there wasn't a noticeable performance issue, and it was much better than loading a blurry or nil image.

A possible optimization here is to re-request the image at it's asset size only if the image comes back as nil.

Pencel answered 18/3, 2017 at 5:57 Comment(1)
Can anyone tell me the solution of this #55489498? I am also facing the same issue with Videos.Finnie
P
6

I 've tried many things

  • targetSize greater than (400, 400): not work
  • targetSize equals to asset size: not work
  • Disable Optimize Storage in iCloud Photos in Settings: not work
  • Dispatch requestImage to background queue: not work
  • Use PHImageManagerMaximumSize: not work
  • Use isNetworkAccessAllowed: not work
  • Play with different values in PHImageRequestOptions, like version, deliveryMode, resizeMode: not work
  • Add a progressHandler: not work
  • Call requestImage again it cases it failed: not work

All I get is nil UIImage and info with PHImageResultDeliveredImageFormatKey, like in this radar Photos Frameworks returns no error or image for particular assets

Use aspect fit

What work for me, see https://github.com/hyperoslo/Gallery/blob/master/Sources/Images/Image.swift#L34

  • Use targetSize with < 200: this is why I can load the thumbnail
  • Use aspectFit: Specify to contentMode does the trick for me

Here is the code

let options = PHImageRequestOptions()
options.isSynchronous = true
options.isNetworkAccessAllowed = true

var result: UIImage? = nil

PHImageManager.default().requestImage(
  for: asset,
  targetSize: size,
  contentMode: .aspectFit,
  options: options) { (image, _) in
    result = image
}

return result

Fetch asynchronously

The above may cause race condition, so make sure you fetch asynchronously, which means no isSynchronous. Take a look at https://github.com/hyperoslo/Gallery/pull/72

Probative answered 27/11, 2017 at 12:49 Comment(1)
Can you pls look into this #55489498? I am also facing the same issue with Videos.Finnie
C
6

I was seeing this as well, and the only thing that worked for me was setting options.isSynchronous = false. My particular options are:

options.isNetworkAccessAllowed = true
options.deliveryMode = .highQualityFormat
options.version = .current
options.resizeMode = .exact
options.isSynchronous = false
Chantal answered 15/1, 2019 at 20:50 Comment(1)
I am currently facing the this similar problem with testing on iphone 11 pro, ios 13. And options.version = .current helped me, ty! You saved my life!Tonina
C
2

Try to use targetSize greater than (400, 400). It helped me.

Calotte answered 30/6, 2016 at 12:48 Comment(1)
Can anyone tell me the solution of this #55489498? I am also facing the same issue with Videos.Finnie
C
2

What it worked for me was letting PHImageManager loading the asset data synchronously, but from an asynchronous background thread. Simplified it looks like this:

    DispatchQueue.global(qos: .userInitiated).async {
        let requestOptions = PHImageRequestOptions()
        requestOptions.isNetworkAccessAllowed = true
        requestOptions.version = .current
        requestOptions.deliveryMode = .opportunistic
        requestOptions.isSynchronous = true
        PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFit, options: requestOptions) { image, _ in
            DispatchQueue.main.async { /* do something with the image */ }
        }
    }
Cainozoic answered 22/8, 2017 at 10:8 Comment(0)
P
2

Starting with iOS 14, this could also happen if the user has not granted permission to use that particular photo using the limited photos picker. For more information, https://developer.apple.com/documentation/photokit/delivering_a_great_privacy_experience_in_your_photos_app

Precipitous answered 23/5, 2021 at 20:27 Comment(0)
N
1

The following fixed my issue:

let options = PHImageRequestOptions()
options.isSynchronous = false
options.isNetworkAccessAllowed = true
options.deliveryMode = .opportunistic
options.version = .current
options.resizeMode = .exact
Naylor answered 18/11, 2019 at 22:24 Comment(0)
P
0

I was also getting nil for iCloud images. It didn't make a difference if I used requestImage or requestImageData flavor of the method. My problem, it turns out, was that my device was connected to the network via Charles Proxy since I wanted to monitor requests and responses app made. For some reason device couldn't work with iCloud if connected this way. Once I turned off the proxy app could get iCloud images.

Prismatoid answered 20/3, 2018 at 0:5 Comment(0)
S
0

The solution for me was setting a targetSize

var image: UIImage?
let options = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
options.isSynchronous = true
options.resizeMode = PHImageRequestOptionsResizeMode.exact

let targetSize = CGSize(width:1200, height:1200)

PHImageManager.default().requestImage(for: self, targetSize: targetSize, contentMode: PHImageContentMode.aspectFit, options: options) { (receivedImage, info) in

    if let formAnImage = receivedImage
    {
        image = formAnImage     
    }
}

Good coding!

Sarene answered 7/8, 2018 at 14:23 Comment(1)
Can anyone tell me the solution of this #55489498? I am also facing the same issue with Videos.Finnie

© 2022 - 2024 — McMap. All rights reserved.