RequestImage's result handler called twice - ios

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

Related

Can I get absolute URI of video on camera roll and mutate it from my app?

I want to perform an operation on a video in the iphone camera roll, and I require an absolute URI as I will be using ffmpeg natively within my app.
Is it possible to operate on the video in place? or would I need to copy the video to a tmp dir, operate on it, and then write back to the camera roll?
I've read some docs and tutorials and answering below based on that research.
Is it possible to operate on the video in place?
Yes (By copying it to temp dir) and No (to the original location where the video is actually stored)
Take a look at the following image and quote from official docs
Using PhotoKit, you can fetch and cache assets for display and
playback, edit image and video content or manage collections of
assets such as albums, Moments, and Shared Albums.
We don't have direct access to the location where the image/video is stored instead we get raw data or representation using PHAsset and Asset objects are immutable so we can't perform operations directly on it. We would need PHAssetChangeRequest to create, delete, change the metadata for, or edit the content of a Photos asset.
would I need to copy the video to a temp dir, operate on it, and then write back to the camera roll?
Yep, that's the way to go.
If you already fetched the assets, and have the PHFetchResult object try:
var video = PHAsset() // the video to be edited
…
if video.canPerform(.content) { // check if the selected PHAsset can be edited
video.requestContentEditingInput(with: nil, completionHandler: { editingInput, _ in
let videoAsset = editingInput?.audiovisualAsset // get tracks and metadata of the video and start editing
let videoURL = (videoAsset as? AVURLAsset)?.url // This might be nil so better use videoAsset
/*
Start editing your video here
*/
guard let input = editingInput else { return }
let output = PHContentEditingOutput(contentEditingInput: input)
let outputURL = output.renderedContentURL // URL at which you write/export the edited video, it must be a .mov file
let editedVideo = NSData() // suppose your video fileName is editedVideo.mov, I used NSData since I don't know what final edited object will be.
editedVideo.write(to: outputURL, atomically: false)
PHPhotoLibrary.shared().performChanges({
let changeRequest = PHAssetChangeRequest(for: video)
changeRequest.contentEditingOutput = output
})
})
}
Or if you're using default imagePicker, we can get tmp video url using:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let videoURL = info[UIImagePickerController.InfoKey.mediaURL] as! NSURL
print(videoURL) // file is already in tmp folder
let video = info[UIImagePickerController.InfoKey.phAsset] as! PHAsset
// implement the above code using this PHAsset
// your will still need to request photo library changes, and save the edited video and/or delete the older one
}
I implement something like this in my project I hope it will help you.
I show all the items in collection view and perform action on selection , you can also get the url of selected video
func getVideoFromCameraRoll() {
let options = PHFetchOptions()
options.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false) ]
options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.video.rawValue)
videos = PHAsset.fetchAssets(with: options)
videoLibraryCV.reloadData()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return videos.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
let asset = videos!.object(at: indexPath.row)
let width: CGFloat = 150
let height: CGFloat = 150
let size = CGSize(width:width, height:height)
cell.layer.borderWidth = 0.5
cell.layer.borderColor = UIColor.lightGray.cgColor
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: PHImageContentMode.aspectFit, options: nil)
{ (image, userInfo) -> Void in
let imageView = cell.viewWithTag(1) as! UIImageView
imageView.image = image
let labelView = cell.viewWithTag(2) as! UILabel
labelView.text = String(format: "%02d:%02d",Int((asset.duration / 60)),Int(asset.duration) % 60)
}
return cell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let asset = photos!.object(at: indexPath.row)
guard(asset.mediaType == PHAssetMediaType.Video)
else {
print("Not a valid video media type")
return
}
PHCachingImageManager().requestAVAssetForVideo(asset, options: nil, resultHandler: {
(asset: AVAsset ? , audioMix : AVAudioMix ? , info : [NSObject: AnyObject] ? ) in
let asset = asset as!AVURLAsset
print(asset.URL) // Here is video URL
})
}
I hope it will work for you ...:)

How to display photo library in collectionView using less memory

I'm facing the problem that people are saying that the application crashes when loading photos library in collectionView. Most of the time they have more than 2-3 thousands of photos. For me it's working fine. (I have less than 1000 photos in my library).
Question: maybe there's any way to display images in collectionView more "smart" and using less memory?
P.S Maybe when user have all of his photos in iCloud that can cause crash too? Because until now I thought that application is not downloading photos when loading it into the cell. Maybe someone can prove or disapprove that fact.
Here's my function:
func grabPhotos(){
let imgManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = false
requestOptions.deliveryMode = .opportunistic // Quality of images
requestOptions.isNetworkAccessAllowed = true
requestOptions.isSynchronous = true
requestOptions.progressHandler = { (progress, error, stop, info) in
print("progress: \(progress)")
}
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
if let fetchResult : PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions) {
if fetchResult.count > 0 {
for i in 0..<fetchResult.count {
imgManager.requestImage(for: fetchResult.object(at: i) , targetSize: CGSize(width: 150, height: 150), contentMode: .aspectFill, options: requestOptions, resultHandler: { image, error in
self.imageArray.append(image!)
self.kolekcija.reloadData()
})
}
}
else {
print("You got no photos")
kolekcija.reloadData()
}
}
}
The problem is this line:
self.imageArray.append(image!)
Never make an array of images. Images are huge, and you'll run out of memory.
This is a collection view. You simply supply the image on demand in itemForRowAt:, and what you supply is a small version of the image (a so-called thumbnail), the smallest possible for the display size. Thus there are, at any time, only enough thumbnails to fill the visible screen; that doesn't take very much memory, plus the runtime can release the image memory when an image is scrolled off the screen.
That is what PHCachingImageManager is for. Look closely at how Apple handles this in their sample code:
https://developer.apple.com/library/archive/samplecode/UsingPhotosFramework/Listings/Shared_AssetGridViewController_swift.html
There is no array of images anywhere in that example.
Working in Swift 5. Get the photo assets, but don't load the photos until cellForItem iteration. Happy coding.
var personalImages: PHFetchResult<AnyObject>!
func getPersonalPhotos() {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
self.personalImages = (PHAsset.fetchAssets(with: .image, options: fetchOptions) as AnyObject?) as! PHFetchResult<AnyObject>?
photoLibraryCollectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return personalImages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "insertCellIdentifierHere", for: indexPath) as! InsertACollectionViewCellHere
let asset: PHAsset = self.personalImages[indexPath.item] as! PHAsset
let itemWidth = photoLibraryCollectionView.frame.size.width / 2
let itemSize = CGSize(width: itemWidth, height: itemWidth / 2) // adjust to your preferences
PHImageManager.default().requestImage(for: asset, targetSize: itemSize, contentMode: .aspectFill, options: nil, resultHandler: {(result, info)in
if result != nil {
cell.photoImageView.image = result
}
})
cell.layoutIfNeeded()
cell.updateConstraintsIfNeeded()
return cell
}

Converting PHAsset to UIImage and displaying it to user

I'm using the BSImagePicker Image Picker controller (See here)
The image picker controller returns PHAsset rather than UIImage, so I found out how to convert PHAsset to UIImage using this function:
PHAsset to UIImage
func getAssetThumbnail(asset: PHAsset) -> UIImage {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
var thumbnail = UIImage()
option.isSynchronous = true
manager.requestImage(for: asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFit, options: option, resultHandler: {(result, info)->Void in
thumbnail = result!
})
return thumbnail
}
Issue is that, I want to return the targetSize of PHImageManagerMaximumSize. Althought when this happens, I get an errorrThis application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.
The error doesn't occur when I set targetSize to CGSize(width: 100, height: 100)
Just you need to do to avoid your error is to force the application to modify the auto layout on the main thread. You can for example use a dispatcher in the main queue when you set your image.
For example:
let image = self.getAssetThumbnail(asset: <YourPHAsset>)
DispatchQueue.main.async {
self.imageView.image = image
}
The error should occur also when you use a targetSize... Strange...

PHAsset image comes pixelated while showing it in CollectionView

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.

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