PHAsset image comes pixelated while showing it in CollectionView - ios

I have this code which I am using in collectionView to get the image.
// Code in CollectionView's cellForItemAtIndex
// Image
PHImageManager.defaultManager().getPhoto(asset, size: cell.thumbnail.frame.size) { (image) in
if let properImage = image
{
dispatch_async(dispatch_get_main_queue(), {
cell.thumbnail.image = properImage
})
}
else
{
cell.thumbnail.image = UIImage()
}
}
And the getPhoto method.
//MARK: PHImageManager
extension PHImageManager
{
func getPhoto( asset : PHAsset, size : CGSize, completion : (image : UIImage?) -> ())
{
let options = PHImageRequestOptions()
options.networkAccessAllowed = true
options.synchronous = false
options.resizeMode = PHImageRequestOptionsResizeMode.Exact
options.deliveryMode = PHImageRequestOptionsDeliveryMode.HighQualityFormat
_ = self.requestImageForAsset(asset, targetSize: size, contentMode: PHImageContentMode.AspectFit, options: options, resultHandler: {
result , _ in
if let resultValue = result as UIImage?
{
completion(image: resultValue)
}
else
{
completion(image: nil)
}
})
}
}
Whenever I load images into collectionView, I can see that the visible cell's images are pixelated. When I scroll thorough the images it appeared properly with high quality image.
Also I have tried all combinations of resizeMode and deliveryMode, didn't help me. I have tried by enabling synchronous too.
I want to show hight quality images in the first time itself. I don't want to scroll through the collectionView to get it.
Is there any way to solve this? Thanks in advance.

You need to scale your image size depending on the scale of your screen like this:
let retinaMultiplier = UIScreen.main.scale
let size = CGSize(width:self.bounds.size.width * retinaMultiplier, height: self.bounds.size.height * retinaMultiplier)
and pass this size into targetSize of your requestImageForAsset method.
And be sure you chose the right options for assets fetching.

Related

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.

RequestImage's result handler called twice

I have an array of image assets. I have to turn those assets into images, add them to an array and upload them to Firebase Database. I have 2 issues with this.
Issue 1:
In a custom UICollectionViewCell I display all the images that the user has selected, I see 4 images in the cell, when I've selected 4 images from the Photos (I'm using a custom framework). Now, when I call requestImage method, I get double the amount of images in the array that's supposed to convert each asset from the asset array and store it into a UIImage array called assetsTurnedIntoImages. I read more about it and it's related to the PHImageRequestOptions and if its isSynchronous property returns true or false, that or if PHImageRequestOptions is nil. Now, obviously I didn't get something because my code still doesn't work.
Issue 2:
As you can see from the code below, the targetSize gives me a somewhat thumbnail image size. When I upload the image to the storage, I don't need a thumbnail, I need it's original size. If I set it to PHImageManagerMaximumSize I get an error:
"Connection to assetsd was interrupted or assetsd died”
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoPostCVCell", for: indexPath) as! PhotoPostCVCell
if let takenImage = cameraPhotoUIImage
{
cell.cellImage.image = takenImage
}
if assets.count > 0
{
let asset = assets[indexPath.row]
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true // synchronous works better when grabbing all images
requestOptions.deliveryMode = .opportunistic
imageManager.requestImage(for: asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFill, options: requestOptions)
{ (image, _) in
DispatchQueue.main.async {
print("WE ARE IN")
cell.cellImage.image = image!
self.assetsTurnedIntoImages.append(image!)
}
}
}
return cell
}
Change the option could solve the problem:
options.deliveryMode = .highQualityFormat
I found that solution in the source code:
#available(iOS 8, iOS 8, *)
public enum PHImageRequestOptionsDeliveryMode : Int {
#available(iOS 8, *)
case opportunistic = 0 // client may get several image results when the call is asynchronous or will get one result when the call is synchronous
#available(iOS 8, *)
case highQualityFormat = 1 // client will get one result only and it will be as asked or better than asked
#available(iOS 8, *)
case fastFormat = 2 // client will get one result only and it may be degraded
}
To avoid completion handler's twice calling, just add an option in this request to make it synchronous
let options = PHImageRequestOptions()
options.isSynchronous = true
let asset: PHAsset = self.photoAsset?[indexPath.item] as! PHAsset
PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 1200, height: 1200), contentMode: .aspectFit, options: options, resultHandler: {(result, info) in
if result != nil {
//do your work here
}
})
To avoid crash on loading image you should compress the image or reduce its size for further work

Using SdWebImage and Image Change immediately by swift

This is Question Video
I have a problem about imageView by using SDWebImage.
I change user's image and already get new user's image url, but when I push to this ViewController, it will show the old image first and change to new image.
What's wrong with me?
Thanks.
var avatar:String = "" // previous VC data pass to here
var photoImageView:UIImageView = { () -> UIImageView in
let ui = GeneratorImageView()
ui.backgroundColor = UIColor.clear
ui.layer.masksToBounds = true
ui.contentMode = .scaleAspectFill
return ui
}()
override func viewDidLoad() {
super.viewDidLoad()
iconImageFromUrl(imageView: iconImageView, url: avatar, isResize: false)
}
func iconImageFromUrl(imageView:UIImageView, url:String,isResize:Bool) {
imageView.setShowActivityIndicator(true)
imageView.setIndicatorStyle(.gray)
imageView.sd_setImage(with: URL(string:url), placeholderImage: nil, options: .lowPriority, progress: nil
, completed: { (image, error, cacheType, url) in
guard image != nil else{
imageView.image = resizeImage(image: #imageLiteral(resourceName: "defaultIcon"), newWidth: 50)
return
}
DispatchQueue.global().async {
let data = try? Data(contentsOf: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
if data != nil
{
if let image = UIImage(data: data!)
{
DispatchQueue.main.async {
if isResize == true{
imageView.image = resizeImage(image: image, newWidth: 250)
}else{
imageView.image = image
}
}
}
}
}
})
}
sd_setImage method is written inside a category of UIImageView. After downloading the image it sets the image on UIImageview on its own and in the completion closure returns the downloaded/cached UIImage as well.
You dont need to create Data from imageUrl and set it again. If you want to resize image, you can do it on the returned image.
Also, you dont need to check the image nil for setting the default image, just pass the resized default image as placeholder image
imageView.sd_setImage(with: URL(string:url), placeholderImage: resizeImage(image: #imageLiteral(resourceName: "defaultIcon"), newWidth: 50), options: .lowPriority, progress: nil
, completed: { (image, error, cacheType, url) in
guard image != nil else {
return
}
if isResize {
imageView.image = resizeImage(image: image, newWidth: 250)
} })

While converting PHAsset to UIImage losing transparency

After I pick an image from Image picker and try to convert PHAsset to UIImage image is losing transparency of the png Image.
I tried searching everywhere but didn't find anything about it.
func getAssetThumbnail(asset: PHAsset) -> UIImage {
let manager = PHImageManager.defaultManager()
let option = PHImageRequestOptions()
var thumbnail = UIImage()
option.synchronous = true
manager.requestImageForAsset(asset, targetSize: CGSize(width: 341.0, height: 182.0), contentMode: .AspectFit, options: option, resultHandler: {(result, info)->Void in
thumbnail = result!
})
return thumbnail
}
Image before
After selecting and setting it to image view
Get the original image data by calling requestImageDataForAsset with PHImageRequestOptions.version = . Original. You can then create the image from UIImage(data: data).
Example:
func getThumbnail(asset: PHAsset) -> UIImage? {
var thumbnail: UIImage?
let manager = PHImageManager.defaultManager()
let options = PHImageRequestOptions()
options.version = .Original
options.synchronous = true
manager.requestImageDataForAsset(asset, options: options) { data, _, _, _ in
if let data = data {
thumbnail = UIImage(data: data)
}
}
return thumbnail
}

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!

Resources