How show all photos in an album with just one UIImageView? - ios

I know this:
imageView.image = aImage
Now I want to show many photos, i.e, all photos in an album.
let fetchResult = PHAsset.fetchAssetsWithLocalIdentifiers(Array(photoIDSet), options: nil)
let imageRequestOptions = PHImageRequestOptions()
imageRequestOptions.synchronous = true
imageRequestOptions.resizeMode = .Fast
fetchResult.enumerateObjectsUsingBlock({
fetchedObject, _, _ in
if let asset = fetchedObject as? PHAsset{
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize: specialSize, contentMode: .AspectFit, options: imageRequestOptions, resultHandler: {
image, _ in
dispatch_async(dispatch_get_main_queue(), {
imageView.image = image
})
})
}
})
The Code above doesn't work as expected. It will just change one time if lucky.
IS there a way?

I find the problem. The whole enumerateObjectsUsingBlock() method run in the one runloop, when the enumerate method run out, it commit just one change to imageView, the last image.
How to fix it? Use - performSelector:withObject:afterDelay: to call the function self at the end of function. Remember leave a exit.

Related

photo library access all photos cause PHImageManager to fetch image thumbnails

Below is how I fetch an image from the library. When I grant photo access to "Selected Photos" in the system settings for the app the image is as expected the largest size available. However, when I change photo access permissions to "All Photos" the code below produces a thumbnail of the original image. The app runs on ios 15. No iCloud is configured. Can anyone tell me please what is going on?
private func loadImage(assetId: String, done: #escaping (Image?) -> Void) {
let fetchResults: PHFetchResult<PHAsset> =
PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil)
guard let asset: PHAsset = fetchResults.firstObject else {
return
}
let manager = PHImageManager()
manager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize,
contentMode: .aspectFit, options: nil) { (uiImage, _) in
if let uiImage = uiImage {
done(Image(uiImage: uiImage))
} else {
done(nil)
}
}
}
In general I can see why image quality depends on level of access. I was not able to find any documentation on this. What I did find though was that the deliveryMode of the request also matters. when I added the following options to the requestImage call the issue was solved.
let requestOptions = PHImageRequestOptions()
requestOptions.deliveryMode = .highQualityFormat
let manager = PHImageManager()
manager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize,contentMode: .aspectFit, options: requestOptions) { (uiImage, _) in
//do something
}

iOS PHImageManager.default().requestImage callback is called twice for the same image

When I try to get an image with specific size, PHImageManager.default().requestImage is called twice with images of different sizes.
Here is the code:
static func load(from asset: PHAsset, targetSize: CGSize? = nil, completion: #escaping (UIImage?)->()) {
let options = PHImageRequestOptions()
options.isSynchronous = false
let id = UUID()
PHImageManager.default().requestImage(for: asset, targetSize: targetSize ?? PHImageManagerMaximumSize, contentMode: .aspectFill,
options: options, resultHandler: { image, _ in
print(id)
runInMain {
completion(image)
}
})
}
I added UUID to check if the same UUID is printed twice.
This is because the first callback returns a thumbnail while the full size image is being loaded.
From the official Apple documentation:
For an asynchronous request, Photos may call your result handler block more than once. Photos first calls the block to provide a low-quality image suitable for displaying temporarily while it prepares a high-quality image. (If low-quality image data is immediately available, the first call may occur before the method returns.) When the high-quality image is ready, Photos calls your result handler again to provide it. If the image manager has already cached the requested image at full quality, Photos calls your result handler only once. The PHImageResultIsDegradedKey key in the result handler’s info parameter indicates when Photos is providing a temporary low-quality image.
Swift 5 It calls only one time with a .deliveryMode = .highQualityFormat
let manager = PHImageManager.default()
var imageRequestOptions: PHImageRequestOptions {
let options = PHImageRequestOptions()
options.version = .current
options.resizeMode = .exact
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
options.isSynchronous = true
return options
}
self.manager.requestImage(for: asset,targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: self.imageRequestOptions) { (thumbnail, info) in
if let img = thumbnail {
print(img)
}
}
Use:
requestOptions.deliveryMode = .highQualityFormat
instead of: requestOptions.deliveryMode = .opportunistic
.opportunistic - Photos automatically provides one or more results in order to balance image quality and responsiveness.

How to get original image using PHImageManager

I am using below code to get original image from PHAsset:
PHImageRequestOptions *requestOptions = [[PHImageRequestOptions alloc] init];
requestOptions.resizeMode = PHImageRequestOptionsResizeModeNone;
requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
[[PHImageManager defaultManager] requestImageForAsset:phasset
targetSize:PHImageManagerMaximumSize
contentMode:PHImageContentModeDefault
options:requestOptions
resultHandler:^void(UIImage *image, NSDictionary *info) {
if (image) {
}
}];
But found that, image size is greater than the original size. I picked 54 MB file but found its size 123.5MB in response.
Using below code to calculate image size:
NSData *imgData = UIImageJPEGRepresentation(image, 1.0);
NSLog(#"img size: %#", [[NSByteCountFormatter new] stringFromByteCount:imgData.length]);
Any idea how to get an original image using [[PHImageManager defaultManager] requestImageForAsset: API.
I am going to share my code in Swift 5.
In my case, the following code works well on iOS 14.5.1.
extension PHAsset {
func getImageData(_ index: Int, completionHandler: #escaping((_ index: Int, _ image: UIImage?)->Void)) {
let options = PHImageRequestOptions()
options.isSynchronous = true
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(for: self, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: options, resultHandler: { (image, info) in
completionHandler(index, image)
})
}
}
Because I call getImageData in background thread and the return value is not correctly ordered, I use a variable named index for calling order and in turn, pass it to completionHanlder so that I can know from where the return value is.
If you don't need index variable, you can delete it.
I wrote a function to get original image below, it worked for me. Hope it works for you, too.
func getAssetThumbnail(asset: PHAsset) -> UIImage {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
var thumbnail = UIImage()
option.isSynchronous = true
option.isNetworkAccessAllowed = true
option.resizeMode = .none
manager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option) { (result, info) in
thumbnail = result!
}
return thumbnail
}
Use following code to get original image. I have code in Swift, you should convert it to Objective-C
PHImageManager.default().requestImage(for: phAssets.firstObject!, targetSize: PHImageManagerMaximumSize, contentMode: PHImageContentMode.default, options: requestOption) { (image, dictResult) in
// Get file path url of original image.
if let url = dictResult!["PHImageFileURLKey"] as? URL {
self.getImageFromFileUrl(url)
}
}
func getImageFromFileUrl(_ fileUrl: URL) {
// Get dat from url, here I have not using catch block, you must use it for swift.
let imageData = try? Data(contentsOf: fileUrl)
// Get image from data
let image = UIImage(data: imageData!)
// Size count, which is same as original image
let size = ByteCountFormatter()
print("image size = \(size.string(fromByteCount: Int64(imageData!.count)))")
}
For details, I have added comment above the line of code.
I hope this will help you.

PHImageManager requestImageForAsset returns nil sometimes for iCloud photos

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)")
}
}
}
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))")
})
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.
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.
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
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
Try to use targetSize greater than (400, 400).
It helped me.
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 */ }
}
}
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
The following fixed my issue:
let options = PHImageRequestOptions()
options.isSynchronous = false
options.isNetworkAccessAllowed = true
options.deliveryMode = .opportunistic
options.version = .current
options.resizeMode = .exact
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.
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!

Photos Framework. requestImageForAsset returning two results. Can't set image view

So I am using the SwipeView library (https://github.com/nicklockwood/SwipeView) to show images using the Photos framework for iOS8.
However, when I call the requestImageForAsset I notice I am getting two results, a thumbnail size, and the bigger size that I want. However, the bigger image isn't loaded (it's called async I understand) in time to return, so it returns the small image.
This code might make more sense.
func swipeView(swipeView: SwipeView!, viewForItemAtIndex index: Int, reusingView view: UIView!) -> UIView! {
let asset: PHAsset = self.photosAsset[index] as PHAsset
var imageView: UIImageView!
let screenSize: CGSize = UIScreen.mainScreen().bounds.size
let targetSize = CGSizeMake(screenSize.width, screenSize.height)
var options = PHImageRequestOptions()
// options.deliveryMode = PHImageRequestOptionsDeliveryMode.Opportunistic
options.resizeMode = PHImageRequestOptionsResizeMode.Exact
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize: targetSize, contentMode: .AspectFill, options: options, resultHandler: {(result, info)in
println("huhuhuh")
println(result.size)
println(info)
imageView = UIImageView(image: result)
})
println("setting view)
return imageView
}
Here is the log output:
Enteredhuhuhuh
(33.5,60.0)
SETTING VIEW
huhuhuh
(320.0,568.0)
As you can see it returns the image view before the big image is recieved. How do I make it return this larger image so it's not showing the thumbnai?
Thanks.
Read header of PHImageManager class
If -[PHImageRequestOptions isSynchronous] returns NO (or options is
nil), resultHandler may be called 1 or more times. Typically in this
case, resultHandler will be called asynchronously on the main thread
with the requested results. However, if deliveryMode =
PHImageRequestOptionsDeliveryModeOpportunistic, resultHandler may be
called synchronously on the calling thread if any image data is
immediately available. If the image data returned in this first pass
is of insufficient quality, resultHandler will be called again,
asychronously on the main thread at a later time with the "correct"
results. If the request is cancelled, resultHandler may not be called
at all. If -[PHImageRequestOptions isSynchronous] returns YES,
resultHandler will be called exactly once, synchronously and on the
calling thread. Synchronous requests cannot be cancelled.
resultHandler for asynchronous requests, always called on main thread
So, what you want to do is that you make resultHandler to be called synchronously
PHImageRequestOptions *option = [PHImageRequestOptions new];
option.synchronous = YES;
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:target contentMode:PHImageContentModeAspectFill options:option resultHandler:^(UIImage *result, NSDictionary *info) {
//this block will be called synchronously
}];
So your block will be called before ending your method
Good luck!
By default the requestImageForAsset works as asynchronous. So in your method the return statement will be executed even before the image is retrieved. So my suggestion is instead of returning the imageView, pass the imageView in that you need to populate the image:
func swipeView(swipeView: SwipeView!, viewForItemAtIndex index: Int, reusingView view: UIView!, yourImageView: UIImageView)
{
let asset: PHAsset = self.photosAsset[index] as PHAsset
var imageView: UIImageView! = yourImageView;
let screenSize: CGSize = UIScreen.mainScreen().bounds.size
let targetSize = CGSizeMake(screenSize.width, screenSize.height)
var options = PHImageRequestOptions()
options.resizeMode = PHImageRequestOptionsResizeMode.Exact
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize: targetSize, contentMode: .AspectFill, options: options, resultHandler: {(result, info)in
imageView.image = result;
})
}
Note:
Another option is you can fire a notification with the result image from the resultHandler. But I prefer the above mentioned method.
Refer PHImageManager for more information.
resultHandler: block has info dictionary which may contain Boolean value for PHImageResultIsDegradedKey key, which indicates whether the result image is a low-quality substitute for the requested image.
Here's what documentation says:
PHImageResultIsDegradedKey: A Boolean value indicating whether the
result image is a low-quality substitute for the requested image.
(NSNumber)
If YES, the result parameter of your resultHandler block contains a
low-quality image because Photos could not yet provide a
higher-quality image. Depending on your settings in the
PHImageRequestOptions object that you provided with the request,
Photos may call your result handler block again to provide a
higher-quality image.
You shouldn't rewrite imageView inside block, just set image to it and everything should work as you expect. To avoid showing two images you can check the size of image before assigning.
var imageView = UIImageView()
...
...
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize: targetSize, contentMode: .AspectFill, options: options, resultHandler: {(result, info)in
println("huhuhuh")
println(result.size)
println(info)
if result.size.width > 1000 && result.size.height > 1000 { // add any size you want
imageView.setImage(result)
}
})
Check the Apple Documentation for this method here. In that they are saying:
For an asynchronous request, Photos may call your result handler block
more than once. Photos first calls the block to provide a low-quality
image suitable for displaying temporarily while it prepares a
high-quality image. (If low-quality image data is immediately
available, the first call may occur before the method returns.)
It might be taking time, in your case for fetching original size image. Otherwise your code seems okay to me.
try this will work for asynchronously too.
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:target contentMode:PHImageContentModeAspectFill options:option resultHandler:^(UIImage *result, NSDictionary *info) {
if ([info objectForKey:#"PHImageFileURLKey"]) {
//your code
}
}];
Use below options, swift 3.0
publi var imageRequestOptions: PHImageRequestOptions{
let options = PHImageRequestOptions()
options.version = .current
options.resizeMode = .exact
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
options.isSynchronous = true
return options}
here is the best solution I had applied and it's working perfectly
let imageManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.deliveryMode = .highQualityFormat
requestOptions.version = .current
requestOptions.resizeMode = .exact
requestOptions.isSynchronous = true
imageManager.requestImage(for: self.assets.first!, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: nil, resultHandler: { image, info in
if let info = info, info["PHImageFileUTIKey"] == nil {
if let isDegraded = info[PHImageResultIsDegradedKey] as? Bool, isDegraded {
//Here is Low quality image , in this case return
print("PHImageResultIsDegradedKey =======> \(isDegraded)")
return
}
//Here you got high resilutions image
}
})

Resources