iOS - Best practice to get async properties of collection in Swift - ios

While working with PHAsset in Swift, I am facing a common problem which must have a good design/solution. For example, I have a collection of PHAsset, lets say assetCollection. Now I want to get total size of the assetCollection which is the sum of all assets in it.
I know there is a asynchronous API to get individual asset size https://stackoverflow.com/a/26551990/1084174 (in objective-c),
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
float imageSize = imageData.length;
//convert to Megabytes
imageSize = imageSize/(1024*1024);
NSLog(#"%f",imageSize);
}];
But when its a collection how do I design the solution?
What's in my mind is, I can run async call inside a loop for each asset in assetCollection summing sizes in total variable until when I get the last result (may be using global variable). total will be the final collection size. But I think there must be some better design/solution to such common problem.
It would be appreciated if anyone suggest.

After spending weeks i have come to a better solution. I am sharing my solution but still there might be some better design. Please share if you get any better.
Solution
I create a wrapper collection, let's say sizedAssetCollection with a property called size. I also implement add() and remove() methods. When I add or remove an item through my add/remove method i calculate update the size on the fly. Like,
void add (PHAsset *asset) {
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
float imageSize = imageData.length;
imageSize = imageSize/(1024*1024);
size += imageSize;
}];
}
void remove(PHAsset *asset) {
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
float imageSize = imageData.length;
imageSize = imageSize/(1024*1024);
size -= imageSize;
}];
}
Finally I get the expected result in size variable.

Related

How to determine the end of all asynchronous calls from within a loop?

I have an app which combines multiple videos, the initial list of PHAssets are displayed and selected to form an array of PHAssets. Now on the screen which creates the video I need to loop through and fetch the AVAsset from the PHAsset.
The issue I am trying to understand is how to track the progress and determine the end of all the asynchronous fetches. When the loop is complete I can move onto actually combining all the videos.
for (PHAsset * object in self.arraySelectedAssets) {
[[PHImageManager defaultManager] requestAVAssetForVideo:object options:nil resultHandler:^(AVAsset *avAsset, AVAudioMix *audioMix, NSDictionary *info) {
NSLog(#"Fetched");
//here asset in nil! IOS 10 only, IOS 11 works fine
AVURLAsset * assetUrl = (AVURLAsset*)avAsset;
}];
}
You can have store the length of self.arraySelectedAssets and have a counter var declared outside the loop.
Then, in each callback you increment the counter variable and also add an if to check if the counter is equal to the length.
In that case you know that you have all the assets.
int total = [self.arraySelectedAssets count];
int count = 0;
for (PHAsset * object in self.arraySelectedAssets) {
[[PHImageManager defaultManager] requestAVAssetForVideo:object options:nil resultHandler:^(AVAsset *avAsset, AVAudioMix *audioMix, NSDictionary *info) {
NSLog(#"Fetched");
//here asset in nil! IOS 10 only, IOS 11 works fine
AVURLAsset * assetUrl = (AVURLAsset*)avAsset;
count ++;
if (count == total) {
//do your stuff
}
}];
}

requestImageDataForAsset returns nil image data

I've been stuck with this for couple of days. I've bee trying to get the image within the call back but I always get nil. These the options which I used:
let options = PHImageRequestOptions()
options.deliveryMode = .HighQualityFormat
options.resizeMode = .None
I also tried with options set to nil without any luck. This is the data I got in the info value passed to the block.
[PHImageResultIsInCloudKey: 0,
PHImageResultDeliveredImageFormatKey: 9999,
PHImageFileURLKey: file:///var/mobile/Media/DCIM/100APPLE/IMG_0052.JPG,
PHImageResultRequestIDKey: 84,
PHImageResultIsDegradedKey: 0,
PHImageResultWantedImageFormatKey: 9999,
PHImageResultIsPlaceholderKey: 0,
PHImageFileSandboxExtensionTokenKey:
64b47b046511a340c57aa1e3e6e07994c1a13853;00000000;00000000;0000001a;com.apple.app-sandbox.read;;00000000;00000000;0000000000000000;/private/var/mobile/Media/DCIM/100APPLE/IMG_0051.JPG]
I also tried using requestImageForAsset and I got the same result. I thought by using requestImageDataForAsset I'll get more control on the data.
Also, I thought the file exists in the cloud, but it is not as PHImageResultIsInCloudKey value is set 0; otherwise, I'd download it.
by the way, I am able to get a smaller version of the image with predefined size of 200x200 inside another view; however, when I try to get the larger version of it, I get nil. I know that the image exists on the phone with higher res (I can see it in the Photos app)
Any help will be appreciated.
I had a similar issue and tried setting the networkAccessAllowed option on the PHImageRequestOptions object to YES - that seemed to fix it. For me the issue was definitely that the images were in the cloud as images on the camera roll worked fine but those in the cloud did not.
I have the same question. I think there is a bug in PHImageRequestOptions class , so we pass nil in the bellow code ,it's helpful for me.
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
assetModel.size = imageData.length;
NSString *filename = [asset valueForKey:#"filename"];
assetModel.fileName = filename;
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

Photos Framework requestImageDataForAsset occasionally fails

I'm using the photos framework on iOS8.1 and requesting the image data for the asset using requestImageDataForAsset... Most of the time it works and I get the image data and a dictionary containing what you see below. But sometimes the call completes, but the data is nil and the dictionary contains three generic looking entries.
The calls are performed sequentially and on the same thread. It is not specific to any particular image. The error will happen on images I've successfully opened in the past. Has anyone encountered this?
+ (NSData *)retrieveAssetDataPhotosFramework:(NSURL *)urlMedia resolution:(CGFloat)resolution imageOrientation:(ALAssetOrientation*)imageOrientation {
__block NSData *iData = nil;
PHFetchResult *result = [PHAsset fetchAssetsWithALAssetURLs:#[urlMedia] options:nil];
PHAsset *asset = [result firstObject];
PHImageManager *imageManager = [PHImageManager defaultManager];
PHImageRequestOptions *options = [[PHImageRequestOptions alloc]init];
options.synchronous = YES;
options.version = PHImageRequestOptionsVersionCurrent;
#autoreleasepool {
[imageManager requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
iData = [imageData copy];
NSLog(#"requestImageDataForAsset returned info(%#)", info);
*imageOrientation = (ALAssetOrientation)orientation;
}];
}
assert(iData.length != 0);
return iData;
}
This is the desired result where I get image data and the dictionary of meta data:
requestImageDataForAsset returned info({
PHImageFileDataKey = <PLXPCShMemData: 0x1702214a0> bufferLength=1753088 dataLength=1749524;
PHImageFileOrientationKey = 1;
PHImageFileSandboxExtensionTokenKey = "6e14948c4d0019fbb4d14cc5e021199f724f0323;00000000;00000000;000000000000001a;com.apple.app-sandbox.read;00000001;01000003;000000000009da80;/private/var/mobile/Media/DCIM/107APPLE/IMG_7258.JPG";
PHImageFileURLKey = "file:///var/mobile/Media/DCIM/107APPLE/IMG_7258.JPG";
PHImageFileUTIKey = "public.jpeg";
PHImageResultDeliveredImageFormatKey = 9999;
PHImageResultIsDegradedKey = 0;
PHImageResultIsInCloudKey = 0;
PHImageResultIsPlaceholderKey = 0;
PHImageResultWantedImageFormatKey = 9999;
})
Here's what I get occasionally. image data is nil. Dictionary contains not so much.
requestImageDataForAsset returned info({
PHImageResultDeliveredImageFormatKey = 9999;
PHImageResultIsDegradedKey = 0;
PHImageResultWantedImageFormatKey = 9999;
})
I had a problem with similar symptoms where requestImageDataForAsset returned nil image data but was also accompanied by a console error message like this:
[Generic] Failed to load image data for asset <PHAsset: 0x13d041940> 87CCAFDC-A0E3-4AC9-AD1C-3F57B897A52E/L0/001 mediaType=1/0, sourceType=2, (113x124), creationDate=2015-06-29 04:56:34 +0000, location=0, hidden=0, favorite=0 with format 9999
In my case, the problem suddenly started happening on a specific device only with assets in iCloud shared albums after upgrading from iOS 10.x to 11.0.3, and since then through to 11.2.5. Thinking that maybe requestImageDataForAsset was trying to use files locally cached in /var/mobile/Media/PhotoData/PhotoCloudSharingData/ (from the info dictionary's PHImageFileURLKey key) and that the cache may be corrupt I thought about how to clear that cache.
Toggling the 'iCloud Photo Sharing' switch in iOS' Settings -> Accounts & Passwords -> iCloud -> Photos seems to have done the trick. requestImageDataForAsset is now working for those previously failing assets.
Update 9th March 2018
I can reproduce this problem now. It seems to occur after restoring a backup from iTunes:
Use the iOS app and retrieve photos from an iCloud shared album.
Backup the iOS device using iTunes.
Restore the backup using iTunes.
Using the app again to retrieve the same photos from the iCloud shared album now fails with the above console message.
Toggling the 'iCloud Photo Sharing' switch fixes it still. Presumably the restore process somehow corrupts some cache. I've reported it as Bug 38290463 to Apple.
You are likely iterating through an array, and memory is not freed timely, you can try the below code. Make sure theData is marked by __block.
#autoreleasepool {
[imageManager requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
NSLog(#"requestImageDataForAsset returned info(%#)", info);
theData = [imageData copy];
}];
}
Getting back to this after a long while, I have solved a big part of my problem. No mystery, just bad code:
PHFetchResult *result = [PHAsset fetchAssetsWithALAssetURLs:#[urlMedia] options:nil];
PHAsset *asset = [result firstObject];
if (asset != nil) { // the fix
PHImageManager *imageManager = [PHImageManager defaultManager];
PHImageRequestOptions *options = [[PHImageRequestOptions alloc]init];
...
}
The most common cause for me was a problem with the media URL passed to fetchAssetsWithALAssetURLs causing asset to be nil and requestImageDataForAsset return a default info object.
The following code maybe help. I think the class PHImageRequestOptions has a bug, so I pass nil , and then fix the bug.
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
assetModel.size = imageData.length;
NSString *filename = [asset valueForKey:#"filename"];
assetModel.fileName = filename;
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

How to know if a PHAsset has been modified?

More specifically, how can you know whether a PHAsset has current version of the underlying asset different than the original?
My user should only need to choose between the current or original asset when necessary. And then I need their answer for PHImageRequestOptions.version.
As of iOS 16, PHAsset has a hasAdjustments property which indicates if the asset has been edited.
For previous releases, you can get an array of data resources for a given asset via PHAssetResource API - it will have an adjustment data resource if that asset has been edited.
let isEdited = PHAssetResource.assetResources(for: asset).contains(where: { $0.type == .adjustmentData })
Note that if you want to actually work with a resource file, you have to fetch its data using a PHAssetResourceManager API. Also note that this method returns right away - there's no waiting for an async network request, unlike other answers here.
I have found two ways of checking PHAsset for modifications.
- (void)tb_checkForModificationsWithEditingInputMethodCompletion:(void (^)(BOOL))completion {
PHContentEditingInputRequestOptions *options = [PHContentEditingInputRequestOptions new];
options.canHandleAdjustmentData = ^BOOL(PHAdjustmentData *adjustmentData) { return YES; };
[self requestContentEditingInputWithOptions:options completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
if (completion) completion(contentEditingInput.adjustmentData != nil);
}];
}
- (void)tb_checkForModificationsWithAssetPathMethodCompletion:(void (^)(BOOL))completion {
PHVideoRequestOptions *options = [PHVideoRequestOptions new];
options.deliveryMode = PHVideoRequestOptionsDeliveryModeFastFormat;
[[PHImageManager defaultManager] requestAVAssetForVideo:self options:options resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if (completion) completion([[asset description] containsString:#"/Mutations/"]);
}];
}
EDIT: I was at the point where I needed the same functionality for PHAsset with an image. I used this:
- (void)tb_checkForModificationsWithAssetPathMethodCompletion:(void (^)(BOOL))completion {
[self requestContentEditingInputWithOptions:nil completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
NSString *path = (contentEditingInput.avAsset) ? [contentEditingInput.avAsset description] : contentEditingInput.fullSizeImageURL.path;
completion([path containsString:#"/Mutations/"]);
}];
}
Take a look at PHImageRequestOptionsVersion
PHImageRequestOptionsVersionCurrent
Request the most recent version of the image asset (the one that reflects all edits).
The resulting image is the rendered output from all previously made adjustments.
PHImageRequestOptionsVersionUnadjusted
Request a version of the image asset without adjustments.
If the asset has been edited, the resulting image reflects the state of the asset before any edits were performed.
PHImageRequestOptionsVersionOriginal
Request the original, highest-fidelity version of the image asset. The
resulting image is originally captured or imported version of the
asset, regardless of any edits made.
If you ask user before retrieving assets, you know which version user specified. If you get a phasset from elsewhere, you can do a revertAssetContentToOriginal to get the original asset. And PHAsset has modificationDate and creationDate properties, you can use this to tell if a PHAsset is modified.
I found this code the only one working for now, and it handles most of the edge cases. It may not be the fastest one but works well for most images types. It takes the smallest possible original and modified image and compare their data content.
#implementation PHAsset (Utilities)
- (void)checkEditingHistoryCompletion:(void (^)(BOOL edited))completion
{
PHImageManager *manager = [PHImageManager defaultManager];
CGSize compareSize = CGSizeMake(64, 48);
PHImageRequestOptions *requestOptions = [PHImageRequestOptions new];
requestOptions.synchronous = YES;
requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeFastFormat;
requestOptions.version = PHImageRequestOptionsVersionOriginal;
[manager requestImageForAsset:self
targetSize:compareSize
contentMode:PHImageContentModeAspectFit
options:requestOptions
resultHandler:^(UIImage *originalResult, NSDictionary *info) {
UIImage *currentImage = originalResult;
requestOptions.version = PHImageRequestOptionsVersionCurrent;
[manager requestImageForAsset:self
targetSize:currentImage.size
contentMode:PHImageContentModeAspectFit
options:requestOptions
resultHandler:^(UIImage *currentResult, NSDictionary *info) {
NSData *currData = UIImageJPEGRepresentation(currentResult, 0.1);
NSData *orgData = UIImageJPEGRepresentation(currentImage, 0.1);
if (completion) {
//handle case when both images cannot be retrived it also mean no edition
if ((currData == nil) && (orgData == nil)) {
completion(NO);
return;
}
completion(([currData isEqualToData:orgData] == NO));
}
}];
}];
}
#end

How can I determine file size on disk of a video PHAsset in iOS8

I can request a video PHAsset using the Photos framework in iOS8. I'd like to know how big the file is on disk. There doesn't seem to be a property of PHAsset to determine that. Does anyone have a solution? (Using Photos framework not required)
Edit
As for iOS 9.3, using requestImageDataForAsset on a video type PHAsset will result in an image, which is the first frame of the video, so it doesn't work anymore. Use the following method instead, for normal video, request option can be nil, but for slow motion video, PHVideoRequestOptionsVersionOriginal needs to be set.
PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
options.version = PHVideoRequestOptionsVersionOriginal;
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:options resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if ([asset isKindOfClass:[AVURLAsset class]]) {
AVURLAsset* urlAsset = (AVURLAsset*)asset;
NSNumber *size;
[urlAsset.URL getResourceValue:&size forKey:NSURLFileSizeKey error:nil];
NSLog(#"size is %f",[size floatValue]/(1024.0*1024.0)); //size is 43.703005
}
}];
//original answer
For PHAsset, use this:
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
float imageSize = imageData.length;
//convert to Megabytes
imageSize = imageSize/(1024*1024);
NSLog(#"%f",imageSize);
}];
For ALAsset:
ALAssetRepresentation *rep = [asset defaultRepresentation];
float imageSize = rep.size/(1024.0*1024.0);
I tested on one video asset, PHAsset shows the size as 43.703125, ALAsset shows the size as 43.703005.
Edit
For PHAsset, another way to get file size. But as #Alfie Hanssen mentioned, it works on normal video, for slow motion video, the following method will return a AVComposition asset in the block, so I added the check for its type. For slow motion video, use the requestImageDataForAsset method.
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:nil resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if ([asset isKindOfClass:[AVURLAsset class]]) {
AVURLAsset* urlAsset = (AVURLAsset*)asset;
NSNumber *size;
[urlAsset.URL getResourceValue:&size forKey:NSURLFileSizeKey error:nil];
NSLog(#"size is %f",[size floatValue]/(1024.0*1024.0)); //size is 43.703005
NSData *data = [NSData dataWithContentsOfURL:urlAsset.URL];
NSLog(#"length %f",[data length]/(1024.0*1024.0)); // data size is 43.703005
}
}];
Swift version with file size formatting:
let options = PHVideoRequestOptions()
options.version = .original
PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { avAsset, _, _ in
if let urlAsset = avAsset as? AVURLAsset { // Could be AVComposition class
if let resourceValues = try? urlAsset.url.resourceValues(forKeys: [.fileSizeKey]),
let fileSize = resourceValues.fileSize {
let formatter = ByteCountFormatter()
formatter.countStyle = .file
let string = formatter.string(fromByteCount: Int64(fileSize))
print(string)
}
}
}
You heave pretty high chance, that video you want to know is's size is not type of AVURLAsset. But it's ok that under the hood there are more files that your video is composited of (for example raw samples, slow-mo time ranges, filters, etc...), because you want to know size of a concrete playable file. I'm not sure how estimated file size meets reality in this case, but this is how it should be done:
PHImageManager.defaultManager().requestExportSessionForVideo(asset, options: nil, exportPreset: AVAssetExportPresetHighestQuality, resultHandler: { (assetExportSession, info) -> Void in // Here you set values that specifies your video (original, after edit, slow-mo, ...) and that affects resulting size.
assetExportSession.timeRange = CMTimeRangeMake(kCMTimeZero, CMTimeMakeWithSeconds(asset.duration, 30)) // Time interval is default from zero to infinite so it needs to be set prior to file size computations. Time scale is I believe (it is "only" preferred value) not important in this case.
let HERE_YOU_ARE = assetExportSession.estimatedOutputFileLength
})

Resources