Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(iOS): 🐛 Image Picker to Handle Partial Failures Gracefully #1639

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 8.1.7
### iOS
Fix Image Picker to Handle Partial Failures Gracefully [#1554](https://github.com/miguelpruivo/flutter_file_picker/issues/1554)

## 8.1.6
### Android
Fix [#1643](https://github.com/miguelpruivo/flutter_file_picker/issues/1643)
Expand Down
226 changes: 116 additions & 110 deletions ios/Classes/FilePickerPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -450,13 +450,13 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking

NSURL *pickedVideoUrl = [info objectForKey:UIImagePickerControllerMediaURL];
NSURL *pickedImageUrl;

if(@available(iOS 13.0, *)) {

if(pickedVideoUrl != nil) {
NSString * fileName = [pickedVideoUrl lastPathComponent];
NSURL * destination = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]];

if([[NSFileManager defaultManager] isReadableFileAtPath: [pickedVideoUrl path]]) {
Log(@"Caching video file for iOS 13 or above...");
[[NSFileManager defaultManager] copyItemAtURL:pickedVideoUrl toURL:destination error:nil];
Expand All @@ -465,152 +465,164 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking
} else {
pickedImageUrl = [info objectForKey:UIImagePickerControllerImageURL];
}

} else {
pickedImageUrl = [info objectForKey:UIImagePickerControllerImageURL];
}

[picker dismissViewControllerAnimated:YES completion:NULL];

if(pickedImageUrl == nil && pickedVideoUrl == nil) {
_result([FlutterError errorWithCode:@"file_picker_error"
message:@"Temporary file could not be created"
details:nil]);
_result = nil;
return;
}

[self handleResult: pickedVideoUrl != nil ? pickedVideoUrl : pickedImageUrl];
}

#ifdef PHPicker

-(void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14)){

if(_result == nil) {
return;
}

if(self.group != nil) {
return;
}


Log(@"Picker:%@ didFinishPicking:%@", picker, results);

[picker dismissViewControllerAnimated:YES completion:nil];

if(results.count == 0) {
Log(@"FilePicker canceled");
_result(nil);
_result = nil;
return;
}

NSMutableArray<NSURL *> * urls = [[NSMutableArray alloc] initWithCapacity: results.count];

NSMutableArray<NSURL *> * urls = [[NSMutableArray alloc] init];
NSMutableArray<NSString *> * errors = [[NSMutableArray alloc] init];

self.group = dispatch_group_create();

// Create image directory if it doesn't exist
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *imagesDir = [documentsPath stringByAppendingPathComponent:@"picked_images"];
NSFileManager *fileManager = [NSFileManager defaultManager];

if (![fileManager fileExistsAtPath:imagesDir]) {
NSError *dirError;
[fileManager createDirectoryAtPath:imagesDir withIntermediateDirectories:YES attributes:nil error:&dirError];
if (dirError) {
Log(@"Failed to create image directory: %@", dirError);
}
}

if(self->_eventSink != nil) {
self->_eventSink([NSNumber numberWithBool:YES]);
}

__block NSError * blockError;

for (NSInteger index = 0; index < results.count; ++index) {
[urls addObject:[NSURL URLWithString:@""]];

// Process images sequentially to avoid memory spikes
dispatch_queue_t processQueue = dispatch_queue_create("com.filepicker.imageprocessing", DISPATCH_QUEUE_SERIAL);
__block NSInteger completedCount = 0;
NSInteger totalCount = results.count;

for (NSInteger index = 0; index < results.count; ++index) {
dispatch_group_enter(_group);
PHPickerResult * result = [results objectAtIndex:index];

dispatch_async(processQueue, ^{
@autoreleasepool {
if (![result.itemProvider hasItemConformingToTypeIdentifier:@"public.image"]) {
[errors addObject:[NSString stringWithFormat:@"Item at index %ld is not an image", (long)index]];
dispatch_group_leave(self->_group);
return;
}

PHPickerResult * result = [results objectAtIndex: index];
[result.itemProvider loadFileRepresentationForTypeIdentifier:@"public.image" completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) {
@autoreleasepool {
if (error != nil || url == nil) {
[errors addObject:[NSString stringWithFormat:@"Failed to load image at index %ld: %@",
(long)index, error ? error.localizedDescription : @"Unknown error"]];
dispatch_group_leave(self->_group);
return;
}

[result.itemProvider loadFileRepresentationForTypeIdentifier:@"public.item" completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) {

if(url == nil) {
blockError = error;
Log("Could not load the picked given file: %@", blockError);
dispatch_group_leave(self->_group);
return;
}

long timestamp = (long)([[NSDate date] timeIntervalSince1970] * 1000);
NSString * filenameWithoutExtension = [url.lastPathComponent stringByDeletingPathExtension];
NSString * fileExtension = url.pathExtension;
NSString * filename = [NSString stringWithFormat:@"%@-%ld.%@", filenameWithoutExtension, timestamp, fileExtension];
NSString * extension = [filename pathExtension];
NSFileManager * fileManager = [[NSFileManager alloc] init];
NSURL * cachedUrl;

// Check for live photos
if(self.allowCompression && [extension isEqualToString:@"pvt"]) {
NSArray * files = [fileManager contentsOfDirectoryAtURL:url includingPropertiesForKeys:@[] options:NSDirectoryEnumerationSkipsHiddenFiles error:nil];

for (NSURL * item in files) {

if (UTTypeConformsTo(UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, CFBridgingRetain([item pathExtension]), NULL), kUTTypeImage)) {
NSData *assetData = [NSData dataWithContentsOfURL:item];
//Convert any type of image to jpeg
NSData *convertedImageData = UIImageJPEGRepresentation([UIImage imageWithData:assetData], 1.0);
//Get meta data from asset
NSDictionary *metaData = [ImageUtils getMetaDataFromImageData:assetData];
//Append meta data into jpeg of live photo
NSData *data = [ImageUtils imageFromImage:convertedImageData withMetaData:metaData];
//Save jpeg
NSString * filenameWithoutExtension = [filename stringByDeletingPathExtension];
NSString * tmpFile = [NSTemporaryDirectory() stringByAppendingPathComponent:[filenameWithoutExtension stringByAppendingString:@".jpeg"]];
cachedUrl = [NSURL fileURLWithPath: tmpFile];

if([fileManager fileExistsAtPath:tmpFile]) {
[fileManager removeItemAtPath:tmpFile error:nil];
@try {
// Create unique filename in app_images directory
NSString *filename = [NSString stringWithFormat:@"image_%@_%ld.%@",
[[NSUUID UUID] UUIDString],
(long)[[NSDate date] timeIntervalSince1970],
url.pathExtension.length > 0 ? url.pathExtension : @"jpg"];

NSString *destinationPath = [imagesDir stringByAppendingPathComponent:filename];
NSURL *destinationUrl = [NSURL fileURLWithPath:destinationPath];

// Load image data with options to reduce memory usage
NSError *loadError = nil;
NSData *imageData = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&loadError];

if (loadError || !imageData) {
[errors addObject:[NSString stringWithFormat:@"Failed to load image data at index %ld: %@",
(long)index, loadError.localizedDescription ?: @"Unknown error"]];
} else {
// Write to destination
if ([imageData writeToURL:destinationUrl options:NSDataWritingAtomic error:&loadError]) {
[urls addObject:destinationUrl];
} else {
[errors addObject:[NSString stringWithFormat:@"Failed to save image at index %ld: %@",
(long)index, loadError.localizedDescription]];
}
}

// Clean up
imageData = nil;

} @catch (NSException *exception) {
[errors addObject:[NSString stringWithFormat:@"Exception processing image at index %ld: %@",
(long)index, exception.description]];
}

if([fileManager createFileAtPath:tmpFile contents:data attributes:nil]) {
filename = tmpFile;
} else {
Log("%@ Error while caching picked Live photo", self);
// Update progress
completedCount++;
if(self->_eventSink != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
self->_eventSink(@{
@"type": @"progress",
@"count": @(completedCount),
@"total": @(totalCount)
});
});
}
break;

dispatch_group_leave(self->_group);
}
}
} else {
NSString * cachedFile = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];

if([fileManager fileExistsAtPath:cachedFile]) {
[fileManager removeItemAtPath:cachedFile error:NULL];
}

cachedUrl = [NSURL fileURLWithPath: cachedFile];

NSError *copyError;
[fileManager copyItemAtURL: url
toURL: cachedUrl
error: &copyError];

if (copyError) {
Log("%@ Error while caching picked file: %@", self, copyError);
return;
}
}];
}


[urls replaceObjectAtIndex:index withObject:cachedUrl];
dispatch_group_leave(self->_group);
}];
});
}

dispatch_group_notify(_group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
self->_group = nil;

if(self->_eventSink != nil) {
self->_eventSink([NSNumber numberWithBool:NO]);
}

if(blockError) {

if (urls.count > 0) {
// If we have at least one successful image, return the results
if (errors.count > 0) {
// Log errors but don't fail the operation
Log(@"Some images failed to process: %@", [errors componentsJoinedByString:@", "]);
}
[self handleResult:urls];
} else {
// Only if all images failed, return an error
self->_result([FlutterError errorWithCode:@"file_picker_error"
message:@"Temporary file could not be created"
details:blockError.description]);
self->_result = nil;
return;
message:@"Failed to process any images"
details:[errors componentsJoinedByString:@"\n"]]);
}
[self handleResult:urls];
self->_result = nil;
});
}

Expand Down Expand Up @@ -665,32 +677,26 @@ - (void)presentationControllerDidDismiss:(UIPresentationController *)controller
#ifdef PICKER_AUDIO
- (void)mediaPickerDidCancel:(MPMediaPickerController *)controller {
Log(@"FilePicker canceled");
if (self.result != nil) {
self.result(nil);
self.result = nil;
}
_result(nil);
_result = nil;
[controller dismissViewControllerAnimated:YES completion:NULL];
}
#endif // PICKER_AUDIO

#ifdef PICKER_DOCUMENT
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
Log(@"FilePicker canceled");
if (self.result != nil) {
self.result(nil);
self.result = nil;
}
_result(nil);
_result = nil;
[controller dismissViewControllerAnimated:YES completion:NULL];
}
#endif // PICKER_DOCUMENT

#ifdef PICKER_MEDIA
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
Log(@"FilePicker canceled");
if (self.result != nil) {
self.result(nil);
self.result = nil;
}
_result(nil);
_result = nil;
[picker dismissViewControllerAnimated:YES completion:NULL];
}
#endif
Expand Down
Loading